Gain Control of Your Desktop with WindowMover : Page 4

WindowMover demonstrates techniques that let you take ultimate control of your desktop. While it focuses on managing window positions for dual monitor systems, you can easily borrow from or extend it for general-purpose UI automation.

by Gigi Sayfan

Apr 24, 2008

Page 4 of 5

The _isMaximized() Method
The _isMaximized() method is a utility function that should have been in the win32gui API wrappers. The Win32 API itself has a function called IsZoomed that tells you whether a window is maximized, but it's missing from win32gui. Luckily, it is pretty easy to reproduce. A window has several generic parts: a title bar, a border, and a client area. The title bar is the strip at the top of the window that contains the title and the minimize/maximize/close buttons. The border is the thin line that surrounds the window and allows you to resize the window by dragging it. The client area is everything in the middle, where all the controls and the content of the window are placed. All these parts are optional, and there are many ways to configure a window, so any particular window may have any combination of these parts, with different styles. It is also possible to override the behavior of these parts with some extra work.

It's important to understand what happens when a window is maximized. The window's size changes so that its client area and the title bar exactly cover the entire screen of the display the window resides in. The border disappears. To determine whether a window is maximized, you can check whether the width of the client area is exactly the width of the monitor. The GetClientRect() API function provides the boundaries of the client area. Unfortunately, it confusingly returns left, top, width, height, while GetWindowRect() returns left, top, right, bottom.

This function is not 100-percent foolproof. A user may resize a window without maximizing it so that its client area exactly covers the monitor; however that's rare, and for WindowMover it is not a use case worth pursuing further.

The _checkTargets() Method
This method contains WindowMover's main algorithm. The full source code for _checkTargets() is in Listing 1, but I'll explain it in smaller chunks:

checkTargets_() prints a little greeting (if in verbose mode) and then calls _getTopLevelWindows() to get the list of top-level windows. Next, it iterates over all the windows using the enumerate() function. This built-in Python function iterates over any iterable object, such as a list or dictionary, and for each item it returns a value pair containing a sequential number and the item itself. You'll find enumerate() very convenient when you want to keep track of how many items you have iterated over so far and what the current item index is. Inside the loop it checks whether the current window is minimized or hidden; if so, it simply continues to the next window (there's no need to move hidden or minimized windows because they don't show up on the primary display).

if w[1] == 'Shell_TrayWnd': # Ignore the tray
continue
move = False
is_target = False
for t in self.targets:
# Check both Window class (w[1]) and title (w[2])
if t in w[1] or t in w[2]:
is_target = True
break
if keep and not is_target:
move = True
if not keep and is_target:
move= True

At the top of the preceding code fragment, you'll see another check—this time for the Shell_TrayWnd. This special window is the system tray and should not be moved. Next, the code initializes the move and is_target variables to False, and then determines whether the current window is a target by iterating the target list, comparing the current window's class and title against each target window. If either matches, the current window is considered a valid target for moving. Note that the comparison checks whether the target is contained in the window's class or title. For example, if the target is cmd.exe, any window whose title contains cmd.exe will match the target. This lets you specify target windows in a flexible way that's less prone to mismatches. After determining that the current window is a target, there is no need to keep checking the other targets, so the code breaks out of the loop.

The final decision about whether to move the current window depends on the two flag variables: is_target and keep. If the current window is a target (is_Target is True) and keep is False, it means that the current window should be moved. If the current window is not a target (is_Target is False), but keep is True, it means that target windows should be kept on the primary display, but the current window should be moved. In all other cases the current window should remain in place.

When a window is a candidate for moving the next step is to find out if the window is maximized. If so, calculate the border width by subtracting the width of the client rectangle (which doesn't include the border) from the window rectangle (which does include the border) and dividing it by two, because the window rectangle includes the width of both the left and right borders:

The final test is to check if the window is actually in the primary display. In this case the primary display is configured to be the right-hand monitor in the system. This means that the horizontal screen coordinates of the primary display are between 0 and monitor_width-1 and the horizontal screen coordinates of the secondary display are between —monitor_width and -1. The check itself just makes sure the left coordinate of the window is greater or equal to —borderWidth. For a non-maximized window the borderWidth is 0, so it's equivalent to check if the borderWidth is greater or equal to 0. For maximized windows, the border stretches outside the primary display and the program accounts for it. That seems unintuitive, because maximized windows seem to have no border, but when you query their size with GetWindowRect() their left co-ordinate is —borderWidth and the client rectangle covers the entire screen—making it only seem as if there is no border. You need to take this little quirk into account when detecting whether the window is in the primary display. For normal windows you don't need to check for —borderWidth. Therefore, to use a single check that works for both types of windows, WindowMover sets the borderWidth variable to 0 for normal windows even though they have a border (note that it doesn't actually set the width of the visual border to 0).

Finally, if the window is indeed on the primary display it gets moved using the _moveWindow() function. If the window is maximized there are some additional gymnastics that change its state (because maximized windows can't be moved) and then maximize the window again after the move.