Auto Window Clicker: Schonzeit für die Maustaste

Andere Rechner - andere Sitten. Wenn man regelmäßig mit zwei verschiedenen Rechnersystemen arbeitet, wünscht man sich sehr bald die jeweils guten Eigenschaften des einen Rechners auch auf dem anderen. Am besten wäre ein Rechner, der alle guten Merkmale beider Systeme in sich vereint...

Neben dem Atari arbeite ich auch noch mit SUNs, auf denen das Fenstersystem X-Windows läuft. Als sehr angenehm habe ich dort die Tatsache empfunden, daß ein Fenster automatisch aktiviert wird, wenn der Mauszeiger in das Fenster fährt. Beim ST muß man in das Fenster klicken. Nun hätte mich der eine Klick nicht weiter gestört, wenn da nicht ein Programm namens 'DC Topper' übers Netz gekommen wäre, welches versprach, die soeben beschriebene Funktion auch auf dem ST zur Verfügung zu stellen. Also das Programm auf Diskette gezogen und auf dem ST gestartet... nichts passiert. Nach einem Blick in das README-File stellt sich heraus, daß es nur im GEM-Desktop funktioniert. Nun gut, dann verlassen wir eben GEMINI und probieren es mit dem originalen Desktop. Und siehe da, es funktioniert.

Da ich aber Programme, die genau mit dem Programm zusammenarbeiten, das ich nicht benutze, ziemlich unnütz finde, landete es nach sehr kurzer Zeit im Papierkorb. Eigentlich schade, denn wenn es mit dem Desktop funktioniert, sollte es eigentlich auch mit anderen Programmen keine Probleme geben.

Man müßte doch nur ...

Ich habe also probiert, das Programm selbst zu schreiben und bin zu einem recht zufriedenstellenden Resultat gekommen. Die Aufgabe des Programms sollte die Simulation eines Mausklicks sein, sobald sich der Mauszeiger über einem nicht aktiven Fenster befindet. Das Programm muß nur bei GEM-Programmen aktiv sein, denn nur diese benutzen Fenster. Und da man auf die Fensterstrukturen legal nur aus GEM-Programmen zugreifen kann, wurde der Clicker ein Accessory und kein AUTO-Ordner-Programm.

Direkt aus der Aufgabenstellung ergab sich die Aufspaltung in zwei Teilprobleme:

  1. Wie erfahre ich, daß unter meinem Mauszeiger ein nicht aktives Window wartet?
  2. Wie simuliere ich dann den Mausklick bzw. aktiviere das Fenster?

Das erste Problem war recht schnell gelöst, da es für alle benötigten Funktionen AES-Aufrufe gibt. Zuerst muß man mit wind_get() das aktuelle Fenster in Erfahrung bringen [siehe Listing 1, Funktion click_main(). Gibt es kein Fenster, sind wir fertig. Andernfalls muß man mit wind_find() das Fenster unter dem Mauszeiger erfragen. Falls es eines gibt und es ungleich dem aktuellen Fenster ist, müssen wir es nur noch aktivieren, womit wir schon beim zweiten Problem angelangt sind. Hier gibt es schon wesentlich mehr Möglichkeiten:

  1. Mit der Funktion Kbshift() stellt man ein, daß die linke Maustaste gedrückt ist; nach einer kleinen Pause stellt man wieder den ursprünglichen Zustand her. Das hört sich eigentlich recht gut an, hat aber den entscheidenden Nachteil, daß es überhaupt nicht funktioniert.

  2. Mit der Funktion appl_tplay() kann man aufgenommene Ereignisse abspielen. Ich spielte das Ereignis Mausklick ab, dies wurde aber erst ausgeführt, nachdem ich auf eine Maustaste drückte. Das machte sich dadurch bemerkbar, daß nach dem Klick in ein nicht aktives Fenster dies aktiv (das ist noch normal) und gleichzeitig ein darin befindliches Objekt selektiert wurde (das passiert erst nach dem zweiten Klick). Offensichtlich war mit dieser Funktion auch nicht mehr herauszuholen.

  3. Nach einem Blick in [1] fand ich die Funktion wind_set(). Damit kann man u. a. ein Fenster in den Vordergrund bringen. Das war eigentlich genau das, was ich wollte. Also ausprobiert... und es funktioniert. Man kann damit tatsächlich ein Fenster einer anderen Applikation manipulieren. Einen kleinen Nachteil will ich hier aber nicht verschweigen: Es gibt Programme (z. B. LASER C), die aus Geschwindigkeitsgründen nicht die standardmäßigen GEM-Slider benutzen. Diese werden bei dieser Methode leider nicht mitgezeichnet. Manch andere Programme merken sich, welches Fenster im Vordergrund ist. Wenn jemand anderes ein Fenster dieses Programmes in den Vordergrund setzt, bekommt das Programm nichts davon mit und geht weiterhin davon aus, daß das alte Fenster noch aktiv ist. Vor allem beim Verschieben kann das zu etwas unerwarteten (aber harmlosen) Effekten führen.

  4. Die kleine Unvollkommenheit der 3. Methode kann man umgehen, wenn man mittels appljwriteO eine Message verschickt, daß das Fenster getopped wurde. Selbst bei LASER C werden die Fenster dann vollständig neu gezeichnet. Leider ist nichts und niemand perfekt, und so hat auch diese Methode ihre gewaltigen Nachteile: Bei appl_write() muß man die Applikations-ID des Empfängers angeben. Es gibt aber keine Funktion, die ermittelt, zu welcher Applikation ein Fenster gehört. Deshalb adressiere ich die Message immer an die Haupt-Applikation (ID 0). Das ist auch meistens richtig so und arbeitet sehr stabil. Falls nun aber ein Accessory-Fenster 'angeklickt' wird, geht die Meldung an das Hauptprogramm. Die Reaktionen darauf sind recht unterschiedlich:

  1. Am besten wäre es doch, wenn man den Mausklick durch Versenden eines Mauspaketes simulieren würde; so wie es vom Tastaturprozessor gemacht wird. In asm_init() wird die Maus-Routine gesucht, der wir das Paket schicken müssen. Diese wird dann zweimal hintereinander aufgerufen: beim ersten Mal wird das Drücken der Taste gemeldet, beim zweiten Mal das Loslassen. Die oben beschriebenen Probleme verschwanden alle, aber dafür gab es ein neues: Befindet sich vor einem inaktiven Fenster eine Dialogbox, werden am laufenden Band Mausklicks simuliert, da das darunterliegende Fenster momentan nicht aktiv werden kann. Gerät der Mauszeiger auf einen Button, so wird dieser unglücklicherweise angewählt. Bei einem heruntergeklappten Menü spielt sich dasselbe ab.

Zum Programm

Im Programm sind die letzten 3 Möglichkeiten verwirklicht. Mit Hilfe von #define kann man wählen, welche Version verwendet werden soll:

TOP_WINDOW
steht für Methode 3, diese ist sehr zu empfehlen.

SEND_MESSAGE
steht für Methode 4, sollte auch mal ausprobiert werden.

ASM_CLICK
steht für die Methode 5, sie ist eigentlich nur interessant, wenn Sie das Programm noch erweitern wollen. Dabei muß man überwachen, ob vor einem Fenster eine Dialogbox oder ein Menü erschienen ist, und dann keinen Klick simulieren.

In der Endlosschleife wird auf MESSAGE- und TIMER-Events gewartet. Pure C bietet hierfür die Funktion EvntMulti() an. Diese arbeitet genau wie der normale evnt_multi(), nur daß anstatt 23 Parametern ein Pointer auf die Parameterliste (Struktur EVENT) übergeben werden muß. Das ist im Hilfesystem nicht erwähnt, aber im Include-File AES.H zu finden.

Trifft eine AC_OPEN-Meldung ein, kann man in einer Alert-Box das Accessory ein- bzw. ausschalten. Der augenblickliche Zustand ist Default. Bei einer AC_CLOSE-Meldung warten wir ein wenig (WAIT_CLOSE gibt die Zeit in ms an). Diese Meldung bekommt ein Accessory immer dann, wenn ein Programm gestartet oder beendet wird. In dieser Zeit läßt der Clicker dann alle Fenster in Ruhe.

Falls ein TIMER-Ereignis eingetreten ist, prüfen wir in click_main(), ob ein Fenster in den Vordergrund zu klicken ist, und tun dies dann in der Funktion do_click(). Die Zeit zwischen zwei TIMER-Events kann man mit WAIT_LOOP festlegen. 500 ms sind meiner Meinung nach recht akzeptabel.

Übersetzen

Das Programm wurde mit dem Pure-C-Compiler-Paket entwickelt. Achten Sie darauf, daß genau ein Makro (TOPJ/VINDOW, SEND_MESSAGE oder ASM_CLICK) definiert ist. Listing 2 ist nur für Version 5 nötig, ansonsten braucht es nicht eingetippt zu werden (dann aber auch aus der Project-Datei entfernen). Wenn Sie Turbo C benutzen, müssen Sie die Namen der Bibliotheken im Project-File entsprechend ändern. Bei anderen Compilern müssen Sie wahrscheinlich EvntMulti() durch den üblichen evnt_multi() ersetzen.

Literatur:
[1] Jankowski, Reschke, Rabich: ATARI ST Profibuch

Der Clicker in Aktion
/**********************************************/ 
/*                                            */
/* Clicker.C              für Clicker.ACC     */
/*                                            */
/* Simuliert einen Mausklick beim Wechsel     */
/* in ein anderes Fenster                     */
/*                                            */
/* V1.0     2. 7.91     Laser C               */
/* V1.1    22. 8.91     Pure C                */
/* V1.2     5. 1.92     ST Computer           */
/*                                            */
/* by Ulrich Mast   (c) 1992 MAXON Computer   */
/*                                            */
/**********************************************/
#include    <tos.h>
#include    <aes.h>
/**********************************************/
#define     FALSE       0
#define     TRUE        1

#define     WAIT_LOOP   500
#define     WAIT_CLOSE 4000

#define     TOP_WINDOW
#undef      SEND_MESSAGE
#undef      ASM_CLICK
/**********************************************/
void        click_main(void);
void        do_click(int);
void        asm_init(void);
long        asm_click(void);
/**********************************************/
int         ap_id;

char *alert[] =
{
    "[1][ | Auto Window Clicker | written 1992 by Uli ] [Off|On]",
    "[3][ | Auto Window Clicker | ist ein Accessory! ][Quit]"
};
/**********************************************/
void
main()
{
    EVENT   event;
    int     active=TRUE;

    ap_id=appl_init();

    if(! app)
    {
        menu_register(ap_id," Clicker ");

#ifdef ASM_CLICK 

    asm_init();

#endif

        while(TRUE)
        {
            event.ev_mflags=MU_MESAG|MU_TIMER; 
            event.ev_mtlocount=WAIT_LOOP; 
            event.ev_mthicount=0;

            EvntMulti(&event);

            if (event.ev_mwich & MU_MESAG)
            {
                if(event.ev_mmgpbuf[0]==AC_OPEN)
                    if(form_alert(active?2:1,alert[0])==2) 
                        active=TRUE; 
                    else
                        active=FALSE;

                if(event.ev_mmgpbuf[0]==AC_CLOSE) 
                    evnt_timer(WAIT_CLOSE,0);
            }
            if(event.ev_mwich & MU_TIMER) 
                if(active)
                    click_main();
        }
    }
    form_alert(1,alert[1]); 
    appl_exit();
}
/**********************************************/
void
click_main() {
{
    int     wind,top;
    int     d; 
    int     x,y;

    wind_get(0,WF_TOP,&top,&d,&d,&d); 
    if(top>0)
    {
        evnt_button(1,0,2,&x,&y,&d,&d); 
        wind=wind_find(x,y); 
        if(wind>0)
            if(wind!=top)
                do_click(wind);
    }
}
/**********************************************/
void
do_click(int wind)
{

#ifdef TOP_WINDOW

    wind_set(wind,WF_TOP,0,0,0,0);
                        /* Fenster aktiv */
#endif

#ifdef SEND_MESSAGE 

    int     msg[8];

    msg[0]=WM_TOPPED;   /* Message          */
    msg[1]=ap_id;       /* Unsere ID        */
    msg[2]=0;           /* keine Überlänge  */
    msg[3]=wind;        /* Fenster ID       */
    msg[4]=0;
    msg[5]=0;
    msg[6]=0;
    msg[7]=0;

    appl_write(0,16,msg); /* -> APP 0       */

#endif

#ifdef ASM_CLICK

    Supexec(asm_click);

#endif

}

Listing 1: Das Hauptprogramm

*************************************************
*                                               
*   Click.S                     für Clicker.ACC
*                                               
*   (c) 1992 MAXON Computer 
*                                               
************************************************

    EXPORT  asm_init 
    EXPORT  asm_click

************************************************
*                                               
* Sucht die Adresse der Maueroutine
*

asm_init:
    movem.l     d0-a6,-(sp)

    move.w      #34,-(sp)           ; Kbdvbase()
    trap        #14 
    addq.l      #2,sp

    add.l       #16,d0              ; KBDVECS.mousevec
    move.l      d0,mouse_vec        ; -> mouse vec

    pea         asm_init_2          ; Supexec (asm_init_2)
    move.w      #38,-(sp)
    trap        #14 
    addq.l      #6,sp

    movem.l     (sp)+,d0-a6 
    rts

asm_init_2:
    lea         mouse_adr,a0        ; Adr. der Mausroutine
    move.l      mouse_vec,a2        ; -> mouse adr
    move.l      (a2),(a0)

    rts

************************************************
*
* Simuliert einen Mausklick der linke Taste
* an der aktuelle Position
*
* Aufruf durch Supexec(asm_click)
*

asm_click:
    movem.l     d0-a6,-(sp)
    move.w      sr,-(sp)
    ori         #$700,sr            ; bitte nicht stören

    move.l      mouse_adr,a2        ; Taste drücken
    lea         druecken,a0
    jsr         (a2)

    move.l      mouse_adr,a2        ; Taste loslassen
    lea         loslassen,a0
    jsr         (a2)

    move.w      (sp)+,sr
    movem.l     (sp)+,d0-a6
    rts

************************************************


            DATA

druecken:
            DC.B $FA,0,0            ; Mauspaket 'Drücken'
            EVEN 
loslassen:
            DC.B $F8,0,0            ; Mauspaket 'Loslassen'
            EVEN 
mouse_vec:
            DC.L 0                  ; Vektor auf Mausroutine
mouse_adr:
            DC.L 0                  ; Adresse der Mausroutine

            END

Listing 2: Die Assembler-Routinen

; Clicker.Prj
;
; Project-Datei für Clicker.ACC unter Pure C
;

clicker.acc     ; das wollen wir haben

.S [-S]         ; mit Privileged Instructions
.L [-F -S=1024] ; ohne Fastload Bit
                ; Stack = 1024 Bytes genügt
=
pcstart.o       ; Startup-Code
clicker.c       ; der C-Source
click.s         ; die Assembler-Routine
                ; kann auch weggelassen werden
                ; (siehe Text)

pcstdlib.lib    ; und die Bibliotheken
pctoslib.lib 
pcgemlib.lib

Listing 3: Das Project-File für Pure C


Ulrich Mast
Links

Copyright-Bestimmungen: siehe Über diese Seite