-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathwindow.cpp
1910 lines (1711 loc) · 94.1 KB
/
window.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
AutoHotkey
Copyright 2003-2008 Chris Mallett ([email protected])
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "stdafx.h" // pre-compiled headers
#include "window.h"
#include "util.h" // for strlcpy()
#include "application.h" // for MsgSleep()
HWND WinActivate(global_struct &aSettings, char *aTitle, char *aText, char *aExcludeTitle, char *aExcludeText
, bool aFindLastMatch, HWND aAlreadyVisited[], int aAlreadyVisitedCount)
{
/*
// If window is already active, be sure to leave it that way rather than activating some
// other window that may match title & text also. NOTE: An explicit check is done
// for this rather than just relying on EnumWindows() to obey the z-order because
// EnumWindows() is *not* guaranteed to enumerate windows in z-order, thus the currently
// active window, even if it's an exact match, might become overlapped by another matching
// window. Also, use the USE_FOREGROUND_WINDOW vs. IF_USE_FOREGROUND_WINDOW macro for
// this because the active window can sometimes be NULL (i.e. if it's a hidden window
// and DetectHiddenWindows is off):
HWND target_window;
if (USE_FOREGROUND_WINDOW(aTitle, aText, aExcludeTitle, aExcludeText))
{
// User asked us to activate the "active" window, which by definition already is.
// However, if the active (foreground) window is hidden and DetectHiddenWindows is
// off, the below will set target_window to be NULL, which seems like the most
// consistent result to use:
SET_TARGET_TO_ALLOWABLE_FOREGROUND(aSettings.DetectHiddenWindows)
return target_window;
}
if (!aFindLastMatch && !*aTitle && !*aText && !*aExcludeTitle && !*aExcludeText)
{
// User passed no params, so use the window most recently found by WinExist():
if ( !(target_window = GetValidLastUsedWindow(aSettings)) )
return NULL;
}
else
{
// Might not help avg. perfomance any?
if (!aFindLastMatch) // Else even if the windows is already active, we want the bottomost one.
if (hwnd = WinActive(aTitle, aText, aExcludeTitle, aExcludeText)) // Already active.
return target_window;
// Don't activate in this case, because the top-most window might be an
// always-on-top but not-meant-to-be-activated window such as AutoIt's
// splash text:
if ( !(target_window = WinExist(aSettings, aTitle, aText, aExcludeTitle, aExcludeText, aFindLastMatch
, false, aAlreadyVisited, aAlreadyVisitedCount)) )
return NULL;
}
// Above has ensured that target_window is non-NULL, that it is a valid window, and that
// it is eligible due to g.DetectHiddenWindows being true or the window not being hidden
// (or being one of the script's GUI windows).
return SetForegroundWindowEx(target_window);
*/
}
#ifdef _DEBUG_WINACTIVATE
#define LOGF "c:\\AutoHotkey SetForegroundWindowEx.txt"
HWND AttemptSetForeground(HWND aTargetWindow, HWND aForeWindow, char *aTargetTitle)
#else
HWND AttemptSetForeground(HWND aTargetWindow, HWND aForeWindow)
#endif
// Returns NULL if aTargetWindow or its owned-window couldn't be brought to the foreground.
// Otherwise, on success, it returns either aTargetWindow or an HWND owned by aTargetWindow.
{
/*
// Probably best not to trust its return value. It's been shown to be unreliable at times.
// Example: I've confirmed that SetForegroundWindow() sometimes (perhaps about 10% of the time)
// indicates failure even though it succeeds. So we specifically check to see if it worked,
// which helps to avoid using the keystroke (2-alts) method, because that may disrupt the
// desired state of the keys or disturb any menus that the user may have displayed.
// Also: I think the 2-alts last-resort may fire when the system is lagging a bit
// (i.e. a drive spinning up) and the window hasn't actually become active yet,
// even though it will soon become active on its own. Also, SetForegroundWindow() sometimes
// indicates failure even though it succeeded, usually because the window didn't become
// active immediately -- perhaps because the system was under load -- but did soon become
// active on its own (after, say, 50ms or so). UPDATE: If SetForegroundWindow() is called
// on a hung window, at least when AttachThreadInput is in effect and that window has
// a modal dialog (such as MSIE's find dialog), this call might never return, locking up
// our thread. So now we do this fast-check for whether the window is hung first (and
// this call is indeed very fast: its worst case is at least 30x faster than the worst-case
// performance of the ABORT-IF-HUNG method used with SendMessageTimeout.
// UPDATE for v1.0.42.03: To avoid a very rare crashing issue, IsWindowHung() is no longer called
// here, but instead by our caller. Search on "v1.0.42.03" for more comments.
BOOL result = SetForegroundWindow(aTargetWindow);
// Note: Increasing the sleep time below did not help with occurrences of "indicated success
// even though it failed", at least with metapad.exe being activated while command prompt
// and/or AutoIt2's InputBox were active or present on the screen:
SLEEP_WITHOUT_INTERRUPTION(SLEEP_INTERVAL); // Specify param so that it will try to specifically sleep that long.
HWND new_fore_window = GetForegroundWindow();
if (new_fore_window == aTargetWindow)
{
#ifdef _DEBUG_WINACTIVATE
if (!result)
{
FileAppend(LOGF, "SetForegroundWindow() indicated failure even though it succeeded: ", false);
FileAppend(LOGF, aTargetTitle);
}
#endif
return aTargetWindow;
}
if (new_fore_window != aForeWindow && aTargetWindow == GetWindow(new_fore_window, GW_OWNER))
// The window we're trying to get to the foreground is the owner of the new foreground window.
// This is considered to be a success because a window that owns other windows can never be
// made the foreground window, at least if the windows it owns are visible.
return new_fore_window;
// Otherwise, failure:
#ifdef _DEBUG_WINACTIVATE
if (result)
{
FileAppend(LOGF, "SetForegroundWindow() indicated success even though it failed: ", false);
FileAppend(LOGF, aTargetTitle);
}
#endif
return NULL;
*/
}
HWND SetForegroundWindowEx(HWND aTargetWindow)
// Caller must have ensured that aTargetWindow is a valid window or NULL, since we
// don't call IsWindow() here.
{
/*
if (!aTargetWindow)
return NULL; // When called this way (as it is sometimes), do nothing.
// v1.0.42.03: Calling IsWindowHung() once here rather than potentially more than once in AttemptSetForeground()
// solves a crash that is not fully understood, nor is it easily reproduced (it occurs only in release mode,
// not debug mode). It's likely a bug in the API's IsHungAppWindow(), but that is far from confirmed.
DWORD target_thread = GetWindowThreadProcessId(aTargetWindow, NULL);
if (target_thread != g_MainThreadID && IsWindowHung(aTargetWindow)) // Calls to IsWindowHung should probably be avoided if the window belongs to our thread. Relies upon short-circuit boolean order.
return NULL;
#ifdef _DEBUG_WINACTIVATE
char win_name[64];
GetWindowText(aTargetWindow, win_name, sizeof(win_name));
#endif
HWND orig_foreground_wnd = GetForegroundWindow();
// AutoIt3: If there is not any foreground window, then input focus is on the TaskBar.
// MY: It is definitely possible for GetForegroundWindow() to return NULL, even on XP.
if (!orig_foreground_wnd)
orig_foreground_wnd = FindWindow("Shell_TrayWnd", NULL);
if (aTargetWindow == orig_foreground_wnd) // It's already the active window.
return aTargetWindow;
if (IsIconic(aTargetWindow))
// This might never return if aTargetWindow is a hung window. But it seems better
// to do it this way than to use the PostMessage() method, which might not work
// reliably with apps that don't handle such messages in a standard way.
// A minimized window must be restored or else SetForegroundWindow() always(?)
// won't work on it. UPDATE: ShowWindowAsync() would prevent a hang, but
// probably shouldn't use it because we rely on the fact that the message
// has been acted on prior to trying to activate the window (and all Async()
// does is post a message to its queue):
ShowWindow(aTargetWindow, SW_RESTORE);
// This causes more trouble than it's worth. In fact, the AutoIt author said that
// he didn't think it even helped with the IE 5.5 related issue it was originally
// intended for, so it seems a good idea to NOT to this, especially since I'm 80%
// sure it messes up the Z-order in certain circumstances, causing an unexpected
// window to pop to the foreground immediately after a modal dialog is dismissed:
//BringWindowToTop(aTargetWindow); // AutoIt3: IE 5.5 related hack.
HWND new_foreground_wnd;
if (!g_WinActivateForce)
// if (g_os.IsWin95() || (!g_os.IsWin9x() && !g_os.IsWin2000orLater()))) // Win95 or NT
// Try a simple approach first for these two OS's, since they don't have
// any restrictions on focus stealing:
#ifdef _DEBUG_WINACTIVATE
#define IF_ATTEMPT_SET_FORE if (new_foreground_wnd = AttemptSetForeground(aTargetWindow, orig_foreground_wnd, win_name))
#else
#define IF_ATTEMPT_SET_FORE if (new_foreground_wnd = AttemptSetForeground(aTargetWindow, orig_foreground_wnd))
#endif
IF_ATTEMPT_SET_FORE
return new_foreground_wnd;
// Otherwise continue with the more drastic methods below.
// MY: The AttachThreadInput method, when used by itself, seems to always
// work the first time on my XP system, seemingly regardless of whether the
// "allow focus steal" change has been made via SystemParametersInfo()
// (but it seems a good idea to keep the SystemParametersInfo() in effect
// in case Win2k or Win98 needs it, or in case it really does help in rare cases).
// In many cases, this avoids the two SetForegroundWindow() attempts that
// would otherwise be needed; and those two attempts cause some windows
// to flash in the taskbar, such as Metapad and Excel (less frequently) whenever
// you quickly activate another window after activating it first (e.g. via hotkeys).
// So for now, it seems best just to use this method by itself. The
// "two-alts" case never seems to fire on my system? Maybe it will
// on Win98 sometimes.
// Note: In addition to the "taskbar button flashing" annoyance mentioned above
// any SetForegroundWindow() attempt made prior to the one below will,
// as a side-effect, sometimes trigger the need for the "two-alts" case
// below. So that's another reason to just keep it simple and do it this way
// only.
#ifdef _DEBUG_WINACTIVATE
char buf[1024];
#endif
bool is_attached_my_to_fore = false, is_attached_fore_to_target = false;
DWORD fore_thread;
if (orig_foreground_wnd) // Might be NULL from above.
{
// Based on MSDN docs, these calls should always succeed due to the other
// checks done above (e.g. that none of the HWND's are NULL):
fore_thread = GetWindowThreadProcessId(orig_foreground_wnd, NULL);
// MY: Normally, it's suggested that you only need to attach the thread of the
// foreground window to our thread. However, I've confirmed that doing all three
// attaches below makes the attempt much more likely to succeed. In fact, it
// almost always succeeds whereas the one-attach method hardly ever succeeds the first
// time (resulting in a flashing taskbar button due to having to invoke a second attempt)
// when one window is quickly activated after another was just activated.
// AutoIt3: Attach all our input threads, will cause SetForeground to work under 98/Me.
// MSDN docs: The AttachThreadInput function fails if either of the specified threads
// does not have a message queue (My: ok here, since any window's thread MUST have a
// message queue). [It] also fails if a journal record hook is installed. ... Note
// that key state, which can be ascertained by calls to the GetKeyState or
// GetKeyboardState function, is reset after a call to AttachThreadInput. You cannot
// attach a thread to a thread in another desktop. A thread cannot attach to itself.
// Therefore, idAttachTo cannot equal idAttach. Update: It appears that of the three,
// this first call does not offer any additional benefit, at least on XP, so not
// using it for now:
//if (g_MainThreadID != target_thread) // Don't attempt the call otherwise.
// AttachThreadInput(g_MainThreadID, target_thread, TRUE);
if (fore_thread && g_MainThreadID != fore_thread && !IsWindowHung(orig_foreground_wnd))
is_attached_my_to_fore = AttachThreadInput(g_MainThreadID, fore_thread, TRUE) != 0;
if (fore_thread && target_thread && fore_thread != target_thread) // IsWindowHung(aTargetWindow) was called earlier.
is_attached_fore_to_target = AttachThreadInput(fore_thread, target_thread, TRUE) != 0;
}
// The log showed that it never seemed to need more than two tries. But there's
// not much harm in trying a few extra times. The number of tries needed might
// vary depending on how fast the CPU is:
for (int i = 0; i < 5; ++i)
{
IF_ATTEMPT_SET_FORE
{
#ifdef _DEBUG_WINACTIVATE
if (i > 0) // More than one attempt was needed.
{
snprintf(buf, sizeof(buf), "AttachThreadInput attempt #%d indicated success: %s"
, i + 1, win_name);
FileAppend(LOGF, buf);
}
#endif
break;
}
}
// I decided to avoid the quick minimize + restore method of activation. It's
// not that much more effective (if at all), and there are some significant
// disadvantages:
// - This call will often hang our thread if aTargetWindow is a hung window: ShowWindow(aTargetWindow, SW_MINIMIZE)
// - Using SW_FORCEMINIMIZE instead of SW_MINIMIZE has at least one (and probably more)
// side effect: When the window is restored, at least via SW_RESTORE, it is no longer
// maximized even if it was before the minmize. So don't use it.
if (!new_foreground_wnd) // Not successful yet.
{
// Some apps may be intentionally blocking us by having called the API function
// LockSetForegroundWindow(), for which MSDN says "The system automatically enables
// calls to SetForegroundWindow if the user presses the ALT key or takes some action
// that causes the system itself to change the foreground window (for example,
// clicking a background window)." Also, it's probably best to avoid doing
// the 2-alts method except as a last resort, because I think it may mess up
// the state of menus the user had displayed. And of course if the foreground
// app has special handling for alt-key events, it might get confused.
// My original note: "The 2-alts case seems to mess up on rare occasions,
// perhaps due to menu weirdness triggered by the alt key."
// AutoIt3: OK, this is not funny - bring out the extreme measures (usually for 2000/XP).
// Simulate two single ALT keystrokes. UPDATE: This hardly ever succeeds. Usually when
// it fails, the foreground window is NULL (none). I'm going to try an Win-tab instead,
// which selects a task bar button. This seems less invasive than doing an alt-tab
// because not only doesn't it activate some other window first, it also doesn't appear
// to change the Z-order, which is good because we don't want the alt-tab order
// that the user sees to be affected by this. UPDATE: Win-tab isn't doing it, so try
// Alt-tab. Alt-tab doesn't do it either. The window itself (metapad.exe is the only
// culprit window I've found so far) seems to resist being brought to the foreground,
// but later, after the hotkey is released, it can be. So perhaps this is being
// caused by the fact that the user has keys held down (logically or physically?)
// Releasing those keys with a key-up event might help, so try that sometime:
KeyEvent(KEYDOWNANDUP, VK_MENU);
KeyEvent(KEYDOWNANDUP, VK_MENU);
//KeyEvent(KEYDOWN, VK_LWIN);
//KeyEvent(KEYDOWN, VK_TAB);
//KeyEvent(KEYUP, VK_TAB);
//KeyEvent(KEYUP, VK_LWIN);
//KeyEvent(KEYDOWN, VK_MENU);
//KeyEvent(KEYDOWN, VK_TAB);
//KeyEvent(KEYUP, VK_TAB);
//KeyEvent(KEYUP, VK_MENU);
// Also replacing "2-alts" with "alt-tab" below, for now:
#ifndef _DEBUG_WINACTIVATE
new_foreground_wnd = AttemptSetForeground(aTargetWindow, orig_foreground_wnd);
#else // debug mode
IF_ATTEMPT_SET_FORE
FileAppend(LOGF, "2-alts ok: ", false);
else
{
FileAppend(LOGF, "2-alts (which is the last resort) failed. ", false);
HWND h = GetForegroundWindow();
if (h)
{
char fore_name[64];
GetWindowText(h, fore_name, sizeof(fore_name));
FileAppend(LOGF, "Foreground: ", false);
FileAppend(LOGF, fore_name, false);
}
FileAppend(LOGF, ". Was trying to activate: ", false);
}
FileAppend(LOGF, win_name);
#endif
} // if()
// Very important to detach any threads whose inputs were attached above,
// prior to returning, otherwise the next attempt to attach thread inputs
// for these particular windows may result in a hung thread or other
// undesirable effect:
if (is_attached_my_to_fore)
AttachThreadInput(g_MainThreadID, fore_thread, FALSE);
if (is_attached_fore_to_target)
AttachThreadInput(fore_thread, target_thread, FALSE);
// Finally. This one works, solving the problem of the MessageBox window
// having the input focus and being the foreground window, but not actually
// being visible (even though IsVisible() and IsIconic() say it is)! It may
// help with other conditions under which this function would otherwise fail.
// Here's the way the repeat the failure to test how the absence of this line
// affects things, at least on my XP SP1 system:
// y::MsgBox, test
// #e::(some hotkey that activates Windows Explorer)
// Now: Activate explorer with the hotkey, then invoke the MsgBox. It will
// usually be activated but invisible. Also: Whenever this invisible problem
// is about to occur, with or without this fix, it appears that the OS's z-order
// is a bit messed up, because when you dismiss the MessageBox, an unexpected
// window (probably the one two levels down) becomes active rather than the
// window that's only 1 level down in the z-order:
if (new_foreground_wnd) // success.
{
// Even though this is already done for the IE 5.5 "hack" above, must at
// a minimum do it here: The above one may be optional, not sure (safest
// to leave it unless someone can test with IE 5.5).
// Note: I suspect the two lines below achieve the same thing. They may
// even be functionally identical. UPDATE: This may no longer be needed
// now that the first BringWindowToTop(), above, has been disabled due to
// its causing more trouble than it's worth. But seems safer to leave
// this one enabled in case it does resolve IE 5.5 related issues and
// possible other issues:
BringWindowToTop(aTargetWindow);
//SetWindowPos(aTargetWindow, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
return new_foreground_wnd; // Return this rather than aTargetWindow because it's more appropriate.
}
else
return NULL;
*/
}
HWND WinClose(global_struct &aSettings, char *aTitle, char *aText, int aTimeToWaitForClose
, char *aExcludeTitle, char *aExcludeText, bool aKillIfHung)
// Return the HWND of any found-window to the caller so that it has the option of waiting
// for it to become an invalid (closed) window.
{
/*
HWND target_window;
IF_USE_FOREGROUND_WINDOW(aSettings.DetectHiddenWindows, aTitle, aText, aExcludeTitle, aExcludeText)
// Close topmost (better than !F4 since that uses the alt key, effectively resetting
// its status to UP if it was down before. Use WM_CLOSE rather than WM_EXIT because
// I think that's what Alt-F4 sends (and otherwise, app may quit without offering
// a chance to save).
// DON'T DISPLAY a MsgBox (e.g. debugging) before trying to close foreground window.
// Otherwise, it may close the owner of the dialog window (this app), perhaps due to
// split-second timing issues.
else if (*aTitle || *aText || *aExcludeTitle || *aExcludeText)
{
// Since EnumWindows() is *not* guaranteed to start proceed in z-order from topmost to
// bottomost (though it almost certainly does), do it this way to ensure that the
// topmost window is closed in preference to any other windows with the same <aTitle>
// and <aText>:
if ( !(target_window = WinActive(aSettings, aTitle, aText, aExcludeTitle, aExcludeText)) )
if ( !(target_window = WinExist(aSettings, aTitle, aText, aExcludeTitle, aExcludeText)) )
return NULL;
}
else
target_window = GetValidLastUsedWindow(aSettings);
return target_window ? WinClose(target_window, aTimeToWaitForClose, aKillIfHung) : NULL;
*/
}
HWND WinClose(HWND aWnd, int aTimeToWaitForClose, bool aKillIfHung)
{
/*
if (aKillIfHung) // This part is based on the AutoIt3 source.
// Update: Another reason not to wait a long time with the below is that WinKill
// is normally only used when the target window is suspected of being hung. It
// seems bad to wait something like 2 seconds in such a case, when the caller
// probably already knows it's hung.
// Obsolete in light of dedicated hook thread: Because this app is much more sensitive to being
// in a "not-pumping-messages" state, due to the keyboard & mouse hooks, it seems better to wait
// for only 200 ms (e.g. in case the user is gaming and there's a script
// running in the background that uses WinKill, we don't want key and mouse events
// to freeze for a long time).
Util_WinKill(aWnd);
else // Don't kill.
// SC_CLOSE is the same as clicking a window's "X"(close) button or using Alt-F4.
// Although it's a more friendly way to close windows than WM_CLOSE (and thus
// avoids incompatibilities with apps such as MS Visual C++), apps that
// have disabled Alt-F4 processing will not be successfully closed. It seems
// best not to send both SC_CLOSE and WM_CLOSE because some apps with an
// "Unsaved. Are you sure?" type dialog might close down completely rather than
// waiting for the user to confirm. Anyway, it's extrememly rare for a window
// not to respond to Alt-F4 (though it is possible that it handles Alt-F4 in a
// non-standard way, i.e. that sending SC_CLOSE equivalent to Alt-F4
// for windows that handle Alt-F4 manually?) But on the upside, this is nicer
// for apps that upon receiving Alt-F4 do some behavior other than closing, such
// as minimizing to the tray. Such apps might shut down entirely if they received
// a true WM_CLOSE, which is probably not what the user would want.
// Update: Swithced back to using WM_CLOSE so that instances of AutoHotkey
// can be terminated via another instances use of the WinClose command:
//PostMessage(aWnd, WM_SYSCOMMAND, SC_CLOSE, 0);
PostMessage(aWnd, WM_CLOSE, 0, 0);
if (aTimeToWaitForClose < 0)
aTimeToWaitForClose = 0;
if (!aTimeToWaitForClose)
return aWnd; // v1.0.30: EnumParentActUponAll() relies on the ability to do no delay at all.
// Slight delay. Might help avoid user having to modify script to use WinWaitClose()
// in many cases. UPDATE: But this does a Sleep(0), which won't yield the remainder
// of our thread's timeslice unless there's another app trying to use 100% of the CPU?
// So, in reality it really doesn't accomplish anything because the window we just
// closed won't get any CPU time (unless, perhaps, it receives the close message in
// time to ask the OS for us to yield the timeslice). Perhaps some finer tuning
// of this can be done in the future. UPDATE: Testing of WinActivate, which also
// uses this to do a Sleep(0), reveals that it may in fact help even when the CPU
// isn't under load. Perhaps this is because upon Sleep(0), the OS runs the
// WindowProc's of windows that have messages waiting for them so that appropriate
// action can be taken (which may often be nearly instantaneous, perhaps under
// 1ms for a Window to be logically destroyed even if it hasn't physically been
// removed from the screen?) prior to returning the CPU to our thread:
DWORD start_time = GetTickCount(); // Before doing any MsgSleeps, set this.
//MsgSleep(0); // Always do one small one, see above comments.
// UPDATE: It seems better just to always do one unspecified-interval sleep
// rather than MsgSleep(0), which often returns immediately, probably having
// no effect.
// Remember that once the first call to MsgSleep() is done, a new hotkey subroutine
// may fire and suspend what we're doing here. Such a subroutine might also overwrite
// the values our params, some of which may be in the deref buffer. So be sure not
// to refer to those strings once MsgSleep() has been done, below:
// This is the same basic code used for ACT_WINWAITCLOSE and such:
for (;;)
{
// Seems best to always do the first one regardless of the value
// of aTimeToWaitForClose:
MsgSleep(INTERVAL_UNSPECIFIED);
if (!IsWindow(aWnd)) // It's gone, so we're done.
return aWnd;
// Must cast to int or any negative result will be lost due to DWORD type:
if ((int)(aTimeToWaitForClose - (GetTickCount() - start_time)) <= SLEEP_INTERVAL_HALF)
break;
// Last param 0 because we don't want it to restore the
// current active window after the time expires (in case
// it's suspended). INTERVAL_UNSPECIFIED performs better.
}
return aWnd; // Done waiting.
*/
}
HWND WinActive(global_struct &aSettings, char *aTitle, char *aText, char *aExcludeTitle, char *aExcludeText
, bool aUpdateLastUsed)
// This function must be kept thread-safe because it may be called (indirectly) by hook thread too.
// In addition, it must not change the value of anything in aSettings except when aUpdateLastUsed==true.
{
/*
HWND target_window;
if (g_script.xifwinactive) // ahkx N11
{
int xwin = g_script.xifwinactive(aTitle);
if (xwin)
{
// printf("found %s", aTitle);
return (HWND)xwin;
}
}
if (USE_FOREGROUND_WINDOW(aTitle, aText, aExcludeTitle, aExcludeText))
{
// User asked us if the "active" window is active, which is true if it's not a
// hidden window, or if DetectHiddenWindows is ON:
SET_TARGET_TO_ALLOWABLE_FOREGROUND(aSettings.DetectHiddenWindows)
#define UPDATE_AND_RETURN_LAST_USED_WINDOW(hwnd) \
{\
if (aUpdateLastUsed && hwnd)\
aSettings.hWndLastUsed = hwnd;\
return hwnd;\
}
UPDATE_AND_RETURN_LAST_USED_WINDOW(target_window)
}
HWND fore_win = GetForegroundWindow();
if (!fore_win)
return NULL;
if (!(*aTitle || *aText || *aExcludeTitle || *aExcludeText)) // Use the "last found" window.
return (fore_win == GetValidLastUsedWindow(aSettings)) ? fore_win : NULL;
// Only after the above check should the below be done. This is because "IfWinActive" (with no params)
// should be "true" if one of the script's GUI windows is active:
if (!(aSettings.DetectHiddenWindows || IsWindowVisible(fore_win))) // In this case, the caller's window can't be active.
return NULL;
WindowSearch ws;
ws.SetCandidate(fore_win);
if (ws.SetCriteria(aSettings, aTitle, aText, aExcludeTitle, aExcludeText) && ws.IsMatch()) // aSettings.DetectHiddenWindows was already checked above.
UPDATE_AND_RETURN_LAST_USED_WINDOW(fore_win) // This also does a "return".
else // If the line above didn't return, indicate that the specified window is not active.
return NULL;
*/
}
HWND WinExist(global_struct &aSettings, char *aTitle, char *aText, char *aExcludeTitle, char *aExcludeText
, bool aFindLastMatch, bool aUpdateLastUsed, HWND aAlreadyVisited[], int aAlreadyVisitedCount)
// This function must be kept thread-safe because it may be called (indirectly) by hook thread too.
// In addition, it must not change the value of anything in aSettings except when aUpdateLastUsed==true.
{
/*
if (g_script.xwingetid) // ahkxN11
{
// printf("trying winexist %s", aTitle); // todo: remove this debug line
int xwin = g_script.xwingetid(aTitle);
if (xwin)
{
// printf("exists %s %d", aTitle, xwin); // todo: remove this debug line
return (HWND)xwin;
}
}
HWND target_window;
if (USE_FOREGROUND_WINDOW(aTitle, aText, aExcludeTitle, aExcludeText))
{
// User asked us if the "active" window exists, which is true if it's not a
// hidden window or DetectHiddenWindows is ON:
SET_TARGET_TO_ALLOWABLE_FOREGROUND(aSettings.DetectHiddenWindows)
// Updating LastUsed to be hwnd even if it's NULL seems best for consistency?
// UPDATE: No, it's more flexible not to never set it to NULL, because there
// will be times when the old value is still useful:
UPDATE_AND_RETURN_LAST_USED_WINDOW(target_window);
}
if (!(*aTitle || *aText || *aExcludeTitle || *aExcludeText))
// User passed no params, so use the window most recently found by WinExist().
// It's correct to do this even in this function because it's called by
// WINWAITCLOSE and IFWINEXIST specifically to discover if the Last-Used
// window still exists.
return GetValidLastUsedWindow(aSettings);
WindowSearch ws;
if (!ws.SetCriteria(aSettings, aTitle, aText, aExcludeTitle, aExcludeText)) // No match is possible with these criteria.
return NULL;
ws.mFindLastMatch = aFindLastMatch;
ws.mAlreadyVisited = aAlreadyVisited;
ws.mAlreadyVisitedCount = aAlreadyVisitedCount;
if (ws.mCriteria & CRITERION_ID) // "ahk_id" will be satisified if that HWND still exists and is valid.
{
// Explicitly allow HWND_BROADCAST for all commands that use WinExist (which is just about all
// window commands), even though it's only valid with ScriptPostSendMessage().
// This is because HWND_BROADCAST is probably never used as the HWND for a real window, so there
// should be no danger of any reasonable script ever passing that value in as a real target window,
// which should thus minimize the chance of a crash due to calling various API functions
// with invalid window handles.
if ( ws.mCriterionHwnd != HWND_BROADCAST // It's not exempt from the other checks on the two lines below.
&& (!IsWindow(ws.mCriterionHwnd) // And it's either not a valid window...
// ...or the window is not detectible (in v1.0.40.05, child windows are detectible even if hidden):
|| !(aSettings.DetectHiddenWindows || IsWindowVisible(ws.mCriterionHwnd)
|| (GetWindowLong(ws.mCriterionHwnd, GWL_STYLE) & WS_CHILD))) )
return NULL;
// Otherwise, the window is valid and detectible.
ws.SetCandidate(ws.mCriterionHwnd);
if (!ws.IsMatch()) // Checks if it matches any other criteria: WinTitle, WinText, ExcludeTitle, and anything in the aAlreadyVisited list.
return NULL;
//else fall through to the section below, since ws.mFoundCount and ws.mFoundParent were set by ws.IsMatch().
}
else // aWinTitle doesn't start with "ahk_id". Try to find a matching window.
EnumWindows(EnumParentFind, (LPARAM)&ws);
UPDATE_AND_RETURN_LAST_USED_WINDOW(ws.mFoundParent) // This also does a "return".
*/
}
HWND GetValidLastUsedWindow(global_struct &aSettings)
// If the last found window is one of the script's own GUI windows, it is considered valid even if
// DetectHiddenWindows is ON. Note that this exemption does not apply to things like "IfWinExist,
// My Gui Title", "WinActivate, ahk_id <gui id>", etc.
// A GUI window can become the last found window while DetectHiddenWindows is ON in two ways:
// Gui +LastFound
// The launch of a GUI thread that explicitly set the last found window to be that GUI window.
{
/*
if (!aSettings.hWndLastUsed || !IsWindow(aSettings.hWndLastUsed))
return NULL;
if ( aSettings.DetectHiddenWindows || IsWindowVisible(aSettings.hWndLastUsed)
|| (GetWindowLong(aSettings.hWndLastUsed, GWL_STYLE) & WS_CHILD) ) // v1.0.40.05: Child windows (via ahk_id) are always detectible.
return aSettings.hWndLastUsed;
// Otherwise, DetectHiddenWindows is OFF and the window is not visible. Return NULL
// unless this is a GUI window belonging to this particular script, in which case
// the setting of DetectHiddenWindows is ignored (as of v1.0.25.13).
return GuiType::FindGui(aSettings.hWndLastUsed) ? aSettings.hWndLastUsed : NULL;
*/
}
BOOL CALLBACK EnumParentFind(HWND aWnd, LPARAM lParam)
// This function must be kept thread-safe because it may be called (indirectly) by hook thread too.
// To continue enumeration, the function must return TRUE; to stop enumeration, it must return FALSE.
// It's a little strange, but I think EnumWindows() returns FALSE when the callback stopped
// the enumeration prematurely by returning false to its caller. Otherwise (the enumeration went
// through every window), it returns TRUE:
{
WindowSearch &ws = *(WindowSearch *)lParam; // For performance and convenience.
// According to MSDN, GetWindowText() will hang only if it's done against
// one of your own app's windows and that window is hung. I suspect
// this might not be true in Win95, and possibly not even Win98, but
// it's not really an issue because GetWindowText() has to be called
// eventually, either here or in an EnumWindowsProc. The only way
// to prevent hangs (if indeed it does hang on Win9x) would be to
// call something like IsWindowHung() before every call to
// GetWindowText(), which might result in a noticeable delay whenever
// we search for a window via its title (or even worse: by the title
// of one of its controls or child windows). UPDATE: Trying GetWindowTextTimeout()
// now, which might be the best compromise. UPDATE: It's annoyingly slow,
// so went back to using the old method.
if (!(ws.mSettings->DetectHiddenWindows || IsWindowVisible(aWnd))) // Skip windows the script isn't supposed to detect.
return TRUE;
ws.SetCandidate(aWnd);
// If this window doesn't match, continue searching for more windows (via TRUE). Likewise, if
// mFindLastMatch is true, continue searching even if this window is a match. Otherwise, this
// first match is the one that's desired so stop here:
return ws.IsMatch() ? ws.mFindLastMatch : TRUE;
}
BOOL CALLBACK EnumChildFind(HWND aWnd, LPARAM lParam)
// This function must be kept thread-safe because it may be called (indirectly) by hook thread too.
// Although this function could be rolled into a generalized version of the EnumWindowsProc(),
// it will perform better this way because there's less checking required and no mode/flag indicator
// is needed inside lParam to indicate which struct element should be searched for. In addition,
// it's more comprehensible this way. lParam is a pointer to the struct rather than just a
// string because we want to give back the HWND of any matching window.
{
// Since WinText and ExcludeText are seldom used in typical scripts, the following buffer
// is put on the stack here rather than on our callers (inside the WindowSearch object),
// which should help conserve stack space on average. Can't use the ws.mCandidateTitle
// buffer because ws.mFindLastMatch might be true, in which case the original title must
// be preserved.
char win_text[WINDOW_TEXT_SIZE];
WindowSearch &ws = *(WindowSearch *)lParam; // For performance and convenience.
if (!(ws.mSettings->DetectHiddenText || IsWindowVisible(aWnd))) // This text element should not be detectible by the script.
return TRUE; // Skip this child and keep enumerating to try to find a match among the other children.
// The below was formerly outsourced to the following function, but since it is only called from here,
// it has been moved inline:
// int GetWindowTextByTitleMatchMode(HWND aWnd, char *aBuf = NULL, int aBufSize = 0)
int text_length = ws.mSettings->TitleFindFast ? GetWindowText(aWnd, win_text, sizeof(win_text))
: GetWindowTextTimeout(aWnd, win_text, sizeof(win_text)); // The slower method that is able to get text from more types of controls (e.g. large edit controls).
// Older idea that for the above that was not adopted:
// Only if GetWindowText() gets 0 length would we try the other method (and of course, don't bother
// using GetWindowTextTimeout() at all if "fast" mode is in effect). The problem with this is that
// many controls always return 0 length regardless of which method is used, so this would slow things
// down a little (but not too badly since GetWindowText() is so much faster than GetWindowTextTimeout()).
// Another potential problem is that some controls may return less text, or different text, when used
// with the fast mode vs. the slow mode (unverified). So it seems best NOT to do this and stick with
// the simple approach above.
if (!text_length) // It has no text (or failure to fetch it).
return TRUE; // Skip this child and keep enumerating to try to find a match among the other children.
// For compatibility with AutoIt v2, strstr() is always used for control/child text elements.
// EXCLUDE-TEXT: The following check takes precedence over the next, so it's done first:
if (*ws.mCriterionExcludeText) // For performance, avoid doing the checks below when blank.
{
if (ws.mSettings->TitleMatchMode == FIND_REGEX)
{
if (RegExMatch(win_text, ws.mCriterionExcludeText))
return FALSE; // Parent can't be a match, so stop searching its children.
}
else // For backward compatibility, all modes other than RegEx behave as follows.
if (strstr(win_text, ws.mCriterionExcludeText))
// Since this child window contains the specified ExcludeText anywhere inside its text,
// the parent window is always a non-match.
return FALSE; // Parent can't be a match, so stop searching its children.
}
// WIN-TEXT:
if (!*ws.mCriterionText) // Match always found in this case. This check is for performance: it avoids doing the checks below when not needed, especially RegEx. Note: It's possible for mCriterionText to be blank, at least when mCriterionExcludeText isn't blank.
{
ws.mFoundChild = aWnd;
return FALSE; // Match found, so stop searching.
}
if (ws.mSettings->TitleMatchMode == FIND_REGEX)
{
if (RegExMatch(win_text, ws.mCriterionText)) // Match found.
{
ws.mFoundChild = aWnd;
return FALSE; // Match found, so stop searching.
}
}
else // For backward compatibility, all modes other than RegEx behave as follows.
if (strstr(win_text, ws.mCriterionText)) // Match found.
{
ws.mFoundChild = aWnd;
return FALSE; // Match found, so stop searching.
}
// UPDATE to the below: The MSDN docs state that EnumChildWindows() already handles the
// recursion for us: "If a child window has created child windows of its own,
// EnumChildWindows() enumerates those windows as well."
// Mostly obsolete comments: Since this child doesn't match, make sure none of its
// children (recursive) match prior to continuing the original enumeration. We don't
// discard the return value from EnumChildWindows() because it's FALSE in two cases:
// 1) The given HWND has no children.
// 2) The given EnumChildProc() stopped prematurely rather than enumerating all the windows.
// and there's no way to distinguish between the two cases without using the
// struct's hwnd because GetLastError() seems to return ERROR_SUCCESS in both
// cases.
//EnumChildWindows(aWnd, EnumChildFind, lParam);
// If matching HWND still hasn't been found, return TRUE to keep searching:
//return ws.mFoundChild == NULL;
return TRUE; // Keep searching.
}
ResultType StatusBarUtil(Var *aOutputVar, HWND aBarHwnd, int aPartNumber, char *aTextToWaitFor
, int aWaitTime, int aCheckInterval)
// aOutputVar is allowed to be NULL if aTextToWaitFor isn't NULL or blank. aBarHwnd is allowed
// to be NULL because in that case, the caller wants us to set ErrorLevel appropriately and also
// make aOutputVar empty.
{
/*
if (aOutputVar)
aOutputVar->Assign(); // Init to blank in case of early return.
// Set default ErrorLevel, which is a special value (2 vs. 1) in the case of StatusBarWait:
g_ErrorLevel->Assign(aOutputVar ? ERRORLEVEL_ERROR : ERRORLEVEL_ERROR2);
// Legacy: Waiting 500ms in place of a "0" seems more useful than a true zero, which doens't need
// to be supported because it's the same thing as something like "IfWinExist":
if (!aWaitTime)
aWaitTime = 500;
if (aCheckInterval < 1)
aCheckInterval = SB_DEFAULT_CHECK_INTERVAL; // Caller relies on us doing this.
if (aPartNumber < 1)
aPartNumber = 1; // Caller relies on us to set default in this case.
// Must have at least one of these. UPDATE: We want to allow this so that the command can be
// used to wait for the status bar text to become blank:
//if (!aOutputVar && !*aTextToWaitFor) return OK;
// Whenever using SendMessageTimeout(), our app will be unresponsive until
// the call returns, since our message loop isn't running. In addition,
// if the keyboard or mouse hook is installed, the input events will lag during
// this call. So keep the timeout value fairly short. Update for v1.0.24:
// There have been at least two reports of the StatusBarWait command ending
// prematurely with an ErrorLevel of 2. The most likely culprit is the below,
// which has now been increased from 100 to 2000:
#define SB_TIMEOUT 2000
HANDLE handle;
LPVOID remote_buf;
LRESULT part_count; // The number of parts this status bar has.
if (!aBarHwnd // These conditions rely heavily on short-circuit boolean order.
|| !SendMessageTimeout(aBarHwnd, SB_GETPARTS, 0, 0, SMTO_ABORTIFHUNG, SB_TIMEOUT, (PDWORD_PTR)&part_count) // It failed or timed out.
|| aPartNumber > part_count
|| !(remote_buf = AllocInterProcMem(handle, WINDOW_TEXT_SIZE + 1, aBarHwnd))) // Alloc mem last.
return OK; // Let ErrorLevel tell the story.
char buf_for_nt[WINDOW_TEXT_SIZE + 1]; // Needed only for NT/2k/XP: the local counterpart to the buf allocated remotely above.
bool is_win9x = g_os.IsWin9x();
char *local_buf = is_win9x ? (char *)remote_buf : buf_for_nt; // Local is the same as remote for Win9x.
DWORD result, start_time;
--aPartNumber; // Convert to zero-based for use below.
// Always do the first iteration so that at least one check is done. Also, start_time is initialized
// unconditionally in the name of code size reduction (it's a low overhead call):
for (*local_buf = '\0', start_time = GetTickCount();;)
{
// MSDN recommends always checking the length of the bar text. It implies that the length is
// unrestricted, so a crash due to buffer overflow could otherwise occur:
if (SendMessageTimeout(aBarHwnd, SB_GETTEXTLENGTH, aPartNumber, 0, SMTO_ABORTIFHUNG, SB_TIMEOUT, &result))
{
// Testing confirms that LOWORD(result) [the length] does not include the zero terminator.
if (LOWORD(result) > WINDOW_TEXT_SIZE) // Text would be too large (very unlikely but good to check for security).
break; // Abort the operation and leave ErrorLevel set to its default to indicate the problem.
// Retrieve the bar's text:
if (SendMessageTimeout(aBarHwnd, SB_GETTEXT, aPartNumber, (LPARAM)remote_buf, SMTO_ABORTIFHUNG, SB_TIMEOUT, &result))
{
if (!is_win9x)
{
if (!ReadProcessMemory(handle, remote_buf, local_buf, LOWORD(result) + 1, NULL)) // +1 to include the terminator (verified: length doesn't include zero terminator).
{
// Fairly critical error (though rare) so seems best to abort.
*local_buf = '\0'; // In case it changed the buf before failing.
break;
}
}
//else Win9x, in which case the local and remote buffers are the same (no copying is needed).
// Check if the retrieved text matches the caller's criteria. In addition to
// normal/intuitive matching, a match is also achieved if both are empty strings.
// In fact, IsTextMatch() yields "true" whenever aTextToWaitFor is the empty string:
if (IsTextMatch(local_buf, aTextToWaitFor))
{
g_ErrorLevel->Assign(ERRORLEVEL_NONE); // Indicate "match found".
break;
}
}
//else SB_GETTEXT msg timed out or failed. Leave local_buf unaltered. See comment below.
}
//else SB_GETTEXTLENGTH msg timed out or failed. For v1.0.37, continue to wait (if other checks
// say its okay below) rather than aborting the operation. This should help prevent an abort
// when the target window (or the entire system) is unresponsive for a long time, perhaps due
// to a drive spinning up, etc.
// Only when above didn't break are the following secondary conditions checked. When aOutputVar
// is non-NULL, the caller wanted a single check only (no waiting) [however, in most such cases,
// the checking above would already have done a "break" because of aTextToWaitFor being blank when
// passed to IsTextMatch()]. Also, don't continue to wait if the status bar no longer exists
// (which is usually caused by the parent window having been destroyed):
if (aOutputVar || !IsWindow(aBarHwnd))
break; // Leave ErrorLevel at its default to indicate bar text retrieval problem in both cases.
// Since above didn't break, we're in "wait" mode (more than one iteration).
// In the following, must cast to int or any negative result will be lost due to DWORD type.
// Note: A negative aWaitTime means we're waiting indefinitely for a match to appear.
if (aWaitTime < 0 || (int)(aWaitTime - (GetTickCount() - start_time)) > SLEEP_INTERVAL_HALF)
MsgSleep(aCheckInterval);
else // Timed out.
{
g_ErrorLevel->Assign(ERRORLEVEL_ERROR); // Override default to indicate timeout vs. "other error".
break;
}
} // for()
// Consider this to be always successful, even if aBarHwnd == NULL
// or the status bar didn't have the part number provided, unless the below fails.
// Note we use a temp buf rather than writing directly to the var contents above, because
// we don't know how long the text will be until after the above operation finishes.
ResultType result_to_return = aOutputVar ? aOutputVar->Assign(local_buf) : OK;
FreeInterProcMem(handle, remote_buf); // Don't free until after the above because above needs file mapping for Win9x.
return result_to_return;
*/
}
HWND ControlExist(HWND aParentWindow, char *aClassNameAndNum)
// This can return target_window itself for cases such as ahk_id %ControlHWND%.
{
/*
if (!aParentWindow)
return NULL;
if (!aClassNameAndNum || !*aClassNameAndNum)
return (GetWindowLong(aParentWindow, GWL_STYLE) & WS_CHILD) ? aParentWindow : GetTopChild(aParentWindow);
// Above: In v1.0.43.06, the parent window itself is returned if it's a child rather than its top child
// because it seems more useful and intuitive. This change allows ahk_id %ControlHwnd% to always operate
// directly on the specified control's HWND rather than some sub-child.
WindowSearch ws;
bool is_class_name = isdigit(aClassNameAndNum[strlen(aClassNameAndNum) - 1]);
if (is_class_name)
{
// Tell EnumControlFind() to search by Class+Num. Don't call ws.SetCriteria() because
// that has special handling for ahk_id, ahk_class, etc. in the first parameter.
strlcpy(ws.mCriterionClass, aClassNameAndNum, sizeof(ws.mCriterionClass));
ws.mCriterionText = "";
}
else // Tell EnumControlFind() to search the control's text.
{
*ws.mCriterionClass = '\0';
ws.mCriterionText = aClassNameAndNum;
}
EnumChildWindows(aParentWindow, EnumControlFind, (LPARAM)&ws); // mFoundChild was initialized by the contructor.
if (is_class_name && !ws.mFoundChild)
{
// To reduce problems with ambiguity (a class name and number of one control happens
// to match the title/text of another control), search again only after the search
// for the ClassNameAndNum didn't turn up anything.
// Tell EnumControlFind() to search the control's text.
*ws.mCriterionClass = '\0';
ws.mCriterionText = aClassNameAndNum;
EnumChildWindows(aParentWindow, EnumControlFind, (LPARAM)&ws); // ws.mFoundChild is already initialized to NULL due to the above check.
}
return ws.mFoundChild;
*/
}
BOOL CALLBACK EnumControlFind(HWND aWnd, LPARAM lParam)
{
/*
WindowSearch &ws = *(WindowSearch *)lParam; // For performance and convenience.
if (*ws.mCriterionClass) // Caller told us to search by class name and number.
{
int length = GetClassName(aWnd, ws.mCandidateTitle, WINDOW_CLASS_SIZE); // Restrict the length to a small fraction of the buffer's size (also serves to leave room to append the sequence number).
// Below: i.e. this control's title (e.g. List) in contained entirely
// within the leading part of the user specified title (e.g. ListBox).
// Even though this is incorrect, the appending of the sequence number
// in the second comparison will weed out any false matches.
// Note: since some controls end in a number (e.g. SysListView32),
// it would not be easy to parse out the user's sequence number to
// simplify/accelerate the search here. So instead, use a method
// more certain to work even though it's a little ugly. It's also
// necessary to do this in a way functionally identical to the below
// so that Window Spy's sequence numbers match the ones generated here:
// Concerning strnicmp(), see lstrcmpi note below for why a locale-insensitive match isn't done instead.
if (length && !strnicmp(ws.mCriterionClass, ws.mCandidateTitle, length)) // Preliminary match of base class name.
{
// mAlreadyVisitedCount was initialized to zero by WindowSearch's constructor. It is used
// to accumulate how many quasi-matches on this class have been found so far. Also,
// comparing ws.mAlreadyVisitedCount to atoi(ws.mCriterionClass + length) would not be
// the same as the below examples such as the following:
// Say the ClassNN being searched for is List01 (where List0 is the class name and 1
// is the sequence number). If a class called "List" exists in the parent window, it
// would be found above as a preliminary match. The below would copy "1" into the buffer,
// which is correctly deemed not to match "01". By contrast, the atoi() method would give
// the wrong result because the two numbers are numerically equal.
_itoa(++ws.mAlreadyVisitedCount, ws.mCandidateTitle, 10); // Overwrite the buffer to contain only the count.
// lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior
// across multiple locales:
if (!stricmp(ws.mCandidateTitle, ws.mCriterionClass + length)) // The counts match too, so it's a full match.
{
ws.mFoundChild = aWnd; // Save this in here for return to the caller.
return FALSE; // stop the enumeration.
}
}
}
else // Caller told us to search by the text of the control (e.g. the text printed on a button)
{
// Use GetWindowText() rather than GetWindowTextTimeout() because we don't want to find
// the name accidentally in the vast amount of text present in some edit controls (e.g.
// if the script's source code is open for editing in notepad, GetWindowText() would
// likely find an unwanted match for just about anything). In addition,
// GetWindowText() is much faster. Update: Yes, it seems better not to use
// GetWindowTextByTitleMatchMode() in this case, since control names tend to be so
// short (i.e. they would otherwise be very likely to be undesirably found in any large
// edit controls the target window happens to own). Update: Changed from strstr()
// to strncmp() for greater selectivity. Even with this degree of selectivity, it's
// still possible to have ambiguous situations where a control can't be found due
// to its title being entirely contained within that of another (e.g. a button
// with title "Connect" would be found in the title of a button "Connect All").
// The only way to address that would be to insist on an entire title match, but