Python win32 programming example: register hotkey and switching tab

Problem description

How you switching task windows in daily using of your computer? I guess is the normal Alt + Tab. But honestly speaking, sometimes it makes you feel annoying because it could be hard and dreadful to locate the right window, especially when you do it very frequently. Actually with a little script, you can be much more happier and more productive on your computer.

This tutorial will shows you a simple Python script to make task switching more efficient. The idea is simple, you will assign some global hotkey to the most frequently used task window so you don't have to go through all active windows to locate the right one with Alt + Tab.

For example: we will assign Shift + F1 to switch to Emacs window which is almost keep active all day long, and assign Shift + F2 to Chrome browser window, also stay on desktop all the time.

Choose the right tool

We have a lot of choices to implement this little tool, for example, the Visual Studio SDK development platform, the Java JNA package, or Python. Many programming languages and platforms can interact with win32 API.

I decide to use Python. With its amazing ctypes module , its so easy and fun to programming win32 api in Python.

Add this to the top of the script:

 
from ctypes import *
 

Now you gain the access to almost all the win32 api in Python.

Step 1. How to enumerate all active windows

Here is the code can accomplish this task:

 
 
EnumWindows = windll.user32.EnumWindows
EnumWindowsProc = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int))
GetWindowText = windll.user32.GetWindowTextW
GetWindowTextLength = windll.user32.GetWindowTextLengthW
IsWindowVisible = windll.user32.IsWindowVisible
GetClassName = windll.user32.GetClassNameW
BringWindowToTop = windll.user32.BringWindowToTop
 
titles = []
def foreach_window(hwnd, lParam):
    if IsWindowVisible(hwnd):
        length = GetWindowTextLength(hwnd)
        classname = create_unicode_buffer(100 + 1)
        GetClassName(hwnd, classname, 100 + 1)
        buff = create_unicode_buffer(length + 1)
        GetWindowText(hwnd, buff, length + 1)
        titles.append((hwnd, buff.value, classname.value)
    return True
EnumWindows(EnumWindowsProc(foreach_window), 0)
 
def refresh_wins():
    del titles[:]
    EnumWindows(EnumWindowsProc(foreach_window), 0)
    return titles
 

The list "titles" contains a list of tuples, each tuple consists of the handle of the window, the title of the window and class name of the window. Since the current active window list is dynamically changing, each time before we try to locate a specified window , we must do the enumeration again to get the newest active window list . This is done by function refresh_wins.

Step 2. How to filter window list

We already get the list of active windows and some property of them. Now we need to filter the list and find out the window we want. In most cases, the window title and window class name provide enough information to identify it. For example the Emacs window has the title like

 
eamcs@username
 

A Chrome browser window has the following format:

 
{page title } - Google Chrome
 

Here is the code to do this :

 
def get_hwnd_by_title_classname(wins, title, classname):
    for item in wins:
        if title in item[1] and classname in item[2]:
            return item[0]
 
def get_hwnd_by_title_classname_cmd(wins, title, classname):
    for item in wins:
        if title in item[1] and classname in item[2] and "clj" not in item[1]:
            return item[0]
 
 
def bring_emacs_top(wins):
    # print (wins)
    hwnd = get_hwnd_by_title_classname(wins, "emacs@", "Emacs")
    if hwnd:
        bring_to_top2(hwnd)
 
 
def bring_chrome_top(wins):
    hwnd = get_hwnd_by_title_classname(wins, " - Google Chrome", "Chrome_WidgetWin")
    if hwnd:
        bring_to_top2(hwnd)
 
def bring_cmd_top(wins):
    hwnd = get_hwnd_by_title_classname_cmd(wins, "cmd -", "ConsoleWindowClass")
    if hwnd:
        bring_to_top2(hwnd)
 
def bring_foxit_top(wins):
    hwnd = get_hwnd_by_title_classname_cmd(wins, "- Foxit Reader", "classFoxitReader")
    if hwnd:
        bring_to_top2(hwnd)
 
 

Step 3. How to bring a window to the top

This part need to import some WIN32 constants:

 
 
import win32service
import win32serviceutil
import win32api
import win32event
import win32evtlogutil
import os
 
import win32con
from win32con import SWP_FRAMECHANGED 
from win32con import SWP_NOMOVE 
from win32con import SWP_NOSIZE 
from win32con import SWP_NOZORDER
from win32con import SW_HIDE
from win32con import SW_FORCEMINIMIZE
from win32con import SW_SHOWNORMAL
 
from win32con import GW_OWNER 
from win32con import GWL_STYLE 
from win32con import GWL_EXSTYLE 
 
from win32con import WM_CLOSE 
 
from win32con import WS_CAPTION 
from win32con import WS_EX_APPWINDOW 
from win32con import WS_EX_CONTROLPARENT
from win32con import WS_EX_TOOLWINDOW
from win32con import WS_EX_WINDOWEDGE
 

You may need different implementation for this task on different platform, for example the following solution will work in WIN8 but may not work on WIN7:

 
def bring_to_top2(hWnd):
    windll.user32.ShowWindow(hWnd,win32con.SW_SHOW); 
    windll.user32.BringWindowToTop(hWnd);
    windll.user32.SetForegroundWindow(hWnd)
 
 

Here are some other solutions, you can choose from them according to your platform

 
 
def bring_to_top(HWND):
    windll.user32.ShowWindow(HWND, win32con.SW_RESTORE)
    windll.user32.SetWindowPos(HWND,win32con.HWND_NOTOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE + win32con.SWP_NOSIZE)  
    windll.user32.SetWindowPos(HWND,win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE + win32con.SWP_NOSIZE)  
    windll.user32.SetWindowPos(HWND,win32con.HWND_NOTOPMOST, 0, 0, 0, 0, win32con.SWP_SHOWWINDOW + win32con.SWP_NOMOVE + win32con.SWP_NOSIZE)
 
 
def bring_to_top3(HWND):
    BringWindowToTop(HWND)
 
 

Step 4. Register hotkey

Thanks to github, there are already an working script for registering global hotkey in win32 paltform. You can search it on github or I just post the source code here : globalhotkeys.py

 
 
import ctypes
import ctypes.wintypes
import win32con
 
 
class GlobalHotKeys(object):
    """
    Register a key using the register() method, or using the @register decorator
    Use listen() to start the message pump
 
    Example:
 
    from globalhotkeys import GlobalHotKeys
 
    @GlobalHotKeys.register(GlobalHotKeys.VK_F1)
    def hello_world():
        print 'Hello World'
 
    GlobalHotKeys.listen()
    """
 
    key_mapping = []
    user32 = ctypes.windll.user32
 
    MOD_ALT = win32con.MOD_ALT
    MOD_CTRL = win32con.MOD_CONTROL
    MOD_CONTROL = win32con.MOD_CONTROL
    MOD_SHIFT = win32con.MOD_SHIFT
    MOD_WIN = win32con.MOD_WIN
 
    @classmethod
    def register(cls, vk, modifier=0, func=None):
        """
        vk is a windows virtual key code
         - can use ord('X') for A-Z, and 0-1 (note uppercase letter only)
         - or win32con.VK_* constants
         - for full list of VKs see: http://msdn.microsoft.com/en-us/library/dd375731.aspx
 
        modifier is a win32con.MOD_* constant
 
        func is the function to run.  If False then break out of the message loop
        """
 
        # Called as a decorator?
        if func is None:
            def register_decorator(f):
                cls.register(vk, modifier, f)
                return f
            return register_decorator
        else:
            cls.key_mapping.append((vk, modifier, func))
 
 
    @classmethod
    def listen(cls):
        """
        Start the message pump
        """
 
        for index, (vk, modifiers, func) in enumerate(cls.key_mapping):
            if not cls.user32.RegisterHotKey(None, index, modifiers, vk):
                raise Exception('Unable to register hot key: ' + str(vk) + ' error code is: ' + str(ctypes.windll.kernel32.GetLastError()))
 
        try:
            msg = ctypes.wintypes.MSG()
            while cls.user32.GetMessageA(ctypes.byref(msg), None, 0, 0) != 0:
                if msg.message == win32con.WM_HOTKEY:
                    (vk, modifiers, func) = cls.key_mapping[msg.wParam]
                    if not func:
                        break
                    func()
 
                cls.user32.TranslateMessage(ctypes.byref(msg))
                cls.user32.DispatchMessageA(ctypes.byref(msg))
 
        finally:
            for index, (vk, modifiers, func) in enumerate(cls.key_mapping):
                cls.user32.UnregisterHotKey(None, index)
 
 
    @classmethod
    def _include_defined_vks(cls):
        for item in win32con.__dict__:
            item = str(item)
            if item[:3] == 'VK_':
                setattr(cls, item, win32con.__dict__[item])
 
 
    @classmethod
    def _include_alpha_numeric_vks(cls):
        for key_code in (list (range(ord('A'), ord('Z') + 1)) + list(range(ord('0'), ord('9') + 1)) ):
            setattr(cls, 'VK_' + chr(key_code), key_code)
 
 
# Not sure if this is really a good idea or not?
#
# It makes decorators look a little nicer, and the user doesn't have to explicitly use win32con (and we add missing VKs
# for A-Z, 0-9
#
# But there no auto-complete (as it's done at run time), and lint'ers hate it
GlobalHotKeys._include_defined_vks()
GlobalHotKeys._include_alpha_numeric_vks()
 
 
 

Here is how to use it

 
@GlobalHotKeys.register(GlobalHotKeys.VK_F1, GlobalHotKeys.MOD_SHIFT)
def shift_f1():
    bring_emacs_top(refresh_wins())
 
 
@GlobalHotKeys.register(GlobalHotKeys.VK_F2, GlobalHotKeys.MOD_SHIFT)
def shift_f2():
    bring_chrome_top(refresh_wins())
 
@GlobalHotKeys.register(GlobalHotKeys.VK_F3, GlobalHotKeys.MOD_SHIFT)
def shift_f3():
    bring_cmd_top(refresh_wins())
 
 
@GlobalHotKeys.register(GlobalHotKeys.VK_F4, GlobalHotKeys.MOD_SHIFT)
def shift_f4():
    bring_foxit_top(refresh_wins())    
 

At the bottom of the script, add this line to start monitoring system key events

 
GlobalHotKeys.listen()
 

Now run the script in console with the command "python hotkey.py". It should be working now.

If you don't want the script hang the console, start it with pythonw.exe , this will run the scirpt as background process.

You may want to run the script as windows service, but it's not possible in WIN8, because the service in Vista and later OS can not interact with the desktop, including register hotkey.

You can auto run the script at system startup.

Run pythonw from bat file

If you don't want type "pythonw.exe hotkey.py" every time, create a bat file with the name you are comfortable for example hotkey.bat

 
@echo off
start pythonw c:\python\hotkey.py
 

Register punctuation character

Recently I feel shift and F1 F2 F3 combination not good enough, because I'm using it so frequently, I want more convenient shortcut. The best one should be CTRL + J ,K and L, but they already been used for more important tasks.

I finally choose ALT + M , . and /, they are conflict with some buffer mode of Emacs, but I rarely use them . But GlobalHotKeys didn't set VK_ attribute for them, we must specify their virtual key code with number.

 
# @GlobalHotKeys.register(GlobalHotKeys.VK_F1, GlobalHotKeys.MOD_SHIFT)
@GlobalHotKeys.register(GlobalHotKeys.VK_M, GlobalHotKeys.MOD_ALT)
def shift_f1():
    bring_emacs_top(refresh_wins())
 
 
#@GlobalHotKeys.register(GlobalHotKeys.VK_F2, GlobalHotKeys.MOD_SHIFT)
#alt + ,
@GlobalHotKeys.register(188, GlobalHotKeys.MOD_ALT)
def shift_f2():
    bring_chrome_top(refresh_wins())
 
#@GlobalHotKeys.register(GlobalHotKeys.VK_F3, GlobalHotKeys.MOD_SHIFT)
#alt + .
@GlobalHotKeys.register(190, GlobalHotKeys.MOD_ALT)
def shift_f3():
    bring_cmd_top(refresh_wins())
 
 
#@GlobalHotKeys.register(GlobalHotKeys.VK_F4, GlobalHotKeys.MOD_SHIFT)
#alt + /
@GlobalHotKeys.register(191, GlobalHotKeys.MOD_ALT)
def shift_f4():
    bring_foxit_top(refresh_wins())
 

You can find more key code here: https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html

Determine whether a window is top window

I want to know whether a window is top window. I looked up MSDN and write this function, but it always returns False.

 
def is_wnd_top(HWND):
    hwnd = windll.user32.GetTopWindow(win32con.NULL)
    return hwnd == HWND
 

When I print the type of the return value hwnd of the function GetTopWindow, it shows its type is int, but according to MSDN, the returned type should be HWND.

But when print the HWND inside foreach_window function, it always c_long

 
 type of hwnd: <class 'int'>
 type of HWND: <class 'ctypes.wintypes.LP_c_long'>
 

Thats why this function fails, compare an int and a c_long always returns False.

Another thing about GetTopWindow is it always returns the "Default IME" window, but from the point of view of the user, the top window should be the cmd window in which we run this python program.

Here we have a little confusion, there are three win32 APIs: GetForegroundWindow , GetActiveWindow , GetTopWindow. What we need is GetForegroundWindow. But in Python, this function still return int type, even the hwnd and the parameter point to same window, the function still return False

 
def is_wnd_top(HWND):
    hwnd = windll.user32.GetForegroundWindow()
    return hwnd == HWND
 

All APIs can accept int or ctypes.wintypes.LP_c_long as HWND parameter, but we can not compare them.

There is an error in our previous code, change the EnumWindowsProc definition to the following

 
EnumWindowsProc = WINFUNCTYPE(c_bool, c_int, POINTER(c_int))
 

See details The story of LP_c_long.

Support toggle hide and show of single window

In current script , when we press the shortcut for a window, it will be brought to foreground. The idea is we should be able to hide the window when we press the shortcut again. So the shortcut not only bring the window to foreground but also can toggle it for hide and show.

This is useful when the window is transparent, I find let the Emacs or Chrome window be transparent is very handy, for example when editing in Emacs, I can see through the Emacs window to monitor the background window, it could be a video playing or a web page loading.

Press shortcut once will hide the current window and show the background window, press it again to bring it back.

Hide a window is simple:

 
    windll.user32.ShowWindow(hwnd,win32con.SW_HIDE); 
 

The problem is when a window is hidden, it will not be enumerated by EnumWindowsProc next time. We need a list to save a window when hide it, and remove from the list when its not hide

 
hidelist = []
 
def find_by_hwnd(winlist,hwnd):
    for index,item in enumerate(winlist):
        if hwnd == item[0]:
            return index
 
def remove_by_hwnd(winlist,hwnd):
    index = find_by_hwnd(winlist,hwnd)
    if index != None:
        winlist.pop(index)
 
 

Add or change the following code

 
 
def refresh_wins():
    del titles[:]
    EnumWindows(EnumWindowsProc(foreach_window), 0)
    titles.extend(hidelist)
    return titles
 
def hide_window(hwnd):
    nextwindow = titles[find_by_hwnd(titles,hwnd) + 1][0] 
 
    windll.user32.ShowWindow(hwnd,win32con.SW_HIDE); 
 
    windll.user32.BringWindowToTop(nextwindow );
    windll.user32.SetForegroundWindow(nextwindow)
 
def bring_to_top2(hWnd):
    if is_wnd_top(hWnd):
        hide_window(hWnd)
        hidelist.append(titles[find_by_hwnd(titles,hWnd)]) 
    else:
        if windll.user32.IsIconic(hWnd):
            windll.user32.OpenIcon(hWnd); 
        else:
            windll.user32.ShowWindow(hWnd,win32con.SW_SHOW); 
 
        windll.user32.BringWindowToTop(hWnd);
        windll.user32.SetForegroundWindow(hWnd)
 
        remove_by_hwnd(hidelist,hWnd)
 
 

The fact is the EnumWindowsProc will enumerate windows by their Z order, when hide the current window, we bring the next window to foreground

 
    nextwindow = titles[find_by_hwnd(titles,hwnd) + 1][0]