459 lines
15 KiB
Python
459 lines
15 KiB
Python
# A demo of a fairly complex dialog.
|
|
#
|
|
# Features:
|
|
# * Uses a "dynamic dialog resource" to build the dialog.
|
|
# * Uses a ListView control.
|
|
# * Dynamically resizes content.
|
|
# * Uses a second worker thread to fill the list.
|
|
# * Demostrates support for windows XP themes.
|
|
|
|
# If you are on Windows XP, and specify a '--noxp' argument, you will see:
|
|
# * alpha-blend issues with icons
|
|
# * The buttons are "old" style, rather than based on the XP theme.
|
|
# Hence, using:
|
|
# import winxpgui as win32gui
|
|
# is recommended.
|
|
# Please report any problems.
|
|
import sys
|
|
|
|
if "--noxp" in sys.argv:
|
|
import win32gui
|
|
else:
|
|
import winxpgui as win32gui
|
|
|
|
import array
|
|
import os
|
|
import queue
|
|
import struct
|
|
|
|
import commctrl
|
|
import win32api
|
|
import win32con
|
|
import win32gui_struct
|
|
import winerror
|
|
|
|
IDC_SEARCHTEXT = 1024
|
|
IDC_BUTTON_SEARCH = 1025
|
|
IDC_BUTTON_DISPLAY = 1026
|
|
IDC_LISTBOX = 1027
|
|
|
|
WM_SEARCH_RESULT = win32con.WM_USER + 512
|
|
WM_SEARCH_FINISHED = win32con.WM_USER + 513
|
|
|
|
|
|
class _WIN32MASKEDSTRUCT:
|
|
def __init__(self, **kw):
|
|
full_fmt = ""
|
|
for name, fmt, default, mask in self._struct_items_:
|
|
self.__dict__[name] = None
|
|
if fmt == "z":
|
|
full_fmt += "pi"
|
|
else:
|
|
full_fmt += fmt
|
|
for name, val in kw.items():
|
|
if name not in self.__dict__:
|
|
raise ValueError("LVITEM structures do not have an item '%s'" % (name,))
|
|
self.__dict__[name] = val
|
|
|
|
def __setattr__(self, attr, val):
|
|
if not attr.startswith("_") and attr not in self.__dict__:
|
|
raise AttributeError(attr)
|
|
self.__dict__[attr] = val
|
|
|
|
def toparam(self):
|
|
self._buffs = []
|
|
full_fmt = ""
|
|
vals = []
|
|
mask = 0
|
|
# calc the mask
|
|
for name, fmt, default, this_mask in self._struct_items_:
|
|
if this_mask is not None and self.__dict__.get(name) is not None:
|
|
mask |= this_mask
|
|
self.mask = mask
|
|
for name, fmt, default, this_mask in self._struct_items_:
|
|
val = self.__dict__[name]
|
|
if fmt == "z":
|
|
fmt = "Pi"
|
|
if val is None:
|
|
vals.append(0)
|
|
vals.append(0)
|
|
else:
|
|
# Note this demo still works with byte strings. An
|
|
# alternate strategy would be to use unicode natively
|
|
# and use the 'W' version of the messages - eg,
|
|
# LVM_SETITEMW etc.
|
|
val = val + "\0"
|
|
if isinstance(val, str):
|
|
val = val.encode("mbcs")
|
|
str_buf = array.array("b", val)
|
|
vals.append(str_buf.buffer_info()[0])
|
|
vals.append(len(val))
|
|
self._buffs.append(str_buf) # keep alive during the call.
|
|
else:
|
|
if val is None:
|
|
val = default
|
|
vals.append(val)
|
|
full_fmt += fmt
|
|
return struct.pack(*(full_fmt,) + tuple(vals))
|
|
|
|
|
|
# NOTE: See the win32gui_struct module for an alternative way of dealing
|
|
# with these structures
|
|
class LVITEM(_WIN32MASKEDSTRUCT):
|
|
_struct_items_ = [
|
|
("mask", "I", 0, None),
|
|
("iItem", "i", 0, None),
|
|
("iSubItem", "i", 0, None),
|
|
("state", "I", 0, commctrl.LVIF_STATE),
|
|
("stateMask", "I", 0, None),
|
|
("text", "z", None, commctrl.LVIF_TEXT),
|
|
("iImage", "i", 0, commctrl.LVIF_IMAGE),
|
|
("lParam", "i", 0, commctrl.LVIF_PARAM),
|
|
("iIdent", "i", 0, None),
|
|
]
|
|
|
|
|
|
class LVCOLUMN(_WIN32MASKEDSTRUCT):
|
|
_struct_items_ = [
|
|
("mask", "I", 0, None),
|
|
("fmt", "i", 0, commctrl.LVCF_FMT),
|
|
("cx", "i", 0, commctrl.LVCF_WIDTH),
|
|
("text", "z", None, commctrl.LVCF_TEXT),
|
|
("iSubItem", "i", 0, commctrl.LVCF_SUBITEM),
|
|
("iImage", "i", 0, commctrl.LVCF_IMAGE),
|
|
("iOrder", "i", 0, commctrl.LVCF_ORDER),
|
|
]
|
|
|
|
|
|
class DemoWindowBase:
|
|
def __init__(self):
|
|
win32gui.InitCommonControls()
|
|
self.hinst = win32gui.dllhandle
|
|
self.list_data = {}
|
|
|
|
def _RegisterWndClass(self):
|
|
className = "PythonDocSearch"
|
|
message_map = {}
|
|
wc = win32gui.WNDCLASS()
|
|
wc.SetDialogProc() # Make it a dialog class.
|
|
wc.hInstance = self.hinst
|
|
wc.lpszClassName = className
|
|
wc.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW
|
|
wc.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW)
|
|
wc.hbrBackground = win32con.COLOR_WINDOW + 1
|
|
wc.lpfnWndProc = message_map # could also specify a wndproc.
|
|
# C code: wc.cbWndExtra = DLGWINDOWEXTRA + sizeof(HBRUSH) + (sizeof(COLORREF));
|
|
wc.cbWndExtra = win32con.DLGWINDOWEXTRA + struct.calcsize("Pi")
|
|
icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
|
|
|
|
## py.ico went away in python 2.5, load from executable instead
|
|
this_app = win32api.GetModuleHandle(None)
|
|
try:
|
|
wc.hIcon = win32gui.LoadIcon(this_app, 1) ## python.exe and pythonw.exe
|
|
except win32gui.error:
|
|
wc.hIcon = win32gui.LoadIcon(this_app, 135) ## pythonwin's icon
|
|
try:
|
|
classAtom = win32gui.RegisterClass(wc)
|
|
except win32gui.error as err_info:
|
|
if err_info.winerror != winerror.ERROR_CLASS_ALREADY_EXISTS:
|
|
raise
|
|
return className
|
|
|
|
def _GetDialogTemplate(self, dlgClassName):
|
|
style = (
|
|
win32con.WS_THICKFRAME
|
|
| win32con.WS_POPUP
|
|
| win32con.WS_VISIBLE
|
|
| win32con.WS_CAPTION
|
|
| win32con.WS_SYSMENU
|
|
| win32con.DS_SETFONT
|
|
| win32con.WS_MINIMIZEBOX
|
|
)
|
|
cs = win32con.WS_CHILD | win32con.WS_VISIBLE
|
|
title = "Dynamic Dialog Demo"
|
|
|
|
# Window frame and title
|
|
dlg = [
|
|
[
|
|
title,
|
|
(0, 0, 210, 250),
|
|
style,
|
|
None,
|
|
(8, "MS Sans Serif"),
|
|
None,
|
|
dlgClassName,
|
|
],
|
|
]
|
|
|
|
# ID label and text box
|
|
dlg.append([130, "Enter something", -1, (5, 5, 200, 9), cs | win32con.SS_LEFT])
|
|
s = cs | win32con.WS_TABSTOP | win32con.WS_BORDER
|
|
dlg.append(["EDIT", None, IDC_SEARCHTEXT, (5, 15, 200, 12), s])
|
|
|
|
# Search/Display Buttons
|
|
# (x positions don't matter here)
|
|
s = cs | win32con.WS_TABSTOP
|
|
dlg.append(
|
|
[
|
|
128,
|
|
"Fill List",
|
|
IDC_BUTTON_SEARCH,
|
|
(5, 35, 50, 14),
|
|
s | win32con.BS_DEFPUSHBUTTON,
|
|
]
|
|
)
|
|
s = win32con.BS_PUSHBUTTON | s
|
|
dlg.append([128, "Display", IDC_BUTTON_DISPLAY, (100, 35, 50, 14), s])
|
|
|
|
# List control.
|
|
# Can't make this work :(
|
|
## s = cs | win32con.WS_TABSTOP
|
|
## dlg.append(['SysListView32', "Title", IDC_LISTBOX, (5, 505, 200, 200), s])
|
|
|
|
return dlg
|
|
|
|
def _DoCreate(self, fn):
|
|
message_map = {
|
|
win32con.WM_SIZE: self.OnSize,
|
|
win32con.WM_COMMAND: self.OnCommand,
|
|
win32con.WM_NOTIFY: self.OnNotify,
|
|
win32con.WM_INITDIALOG: self.OnInitDialog,
|
|
win32con.WM_CLOSE: self.OnClose,
|
|
win32con.WM_DESTROY: self.OnDestroy,
|
|
WM_SEARCH_RESULT: self.OnSearchResult,
|
|
WM_SEARCH_FINISHED: self.OnSearchFinished,
|
|
}
|
|
dlgClassName = self._RegisterWndClass()
|
|
template = self._GetDialogTemplate(dlgClassName)
|
|
return fn(self.hinst, template, 0, message_map)
|
|
|
|
def _SetupList(self):
|
|
child_style = (
|
|
win32con.WS_CHILD
|
|
| win32con.WS_VISIBLE
|
|
| win32con.WS_BORDER
|
|
| win32con.WS_HSCROLL
|
|
| win32con.WS_VSCROLL
|
|
)
|
|
child_style |= (
|
|
commctrl.LVS_SINGLESEL | commctrl.LVS_SHOWSELALWAYS | commctrl.LVS_REPORT
|
|
)
|
|
self.hwndList = win32gui.CreateWindow(
|
|
"SysListView32",
|
|
None,
|
|
child_style,
|
|
0,
|
|
0,
|
|
100,
|
|
100,
|
|
self.hwnd,
|
|
IDC_LISTBOX,
|
|
self.hinst,
|
|
None,
|
|
)
|
|
|
|
child_ex_style = win32gui.SendMessage(
|
|
self.hwndList, commctrl.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0
|
|
)
|
|
child_ex_style |= commctrl.LVS_EX_FULLROWSELECT
|
|
win32gui.SendMessage(
|
|
self.hwndList, commctrl.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, child_ex_style
|
|
)
|
|
|
|
# Add an image list - use the builtin shell folder icon - this
|
|
# demonstrates the problem with alpha-blending of icons on XP if
|
|
# winxpgui is not used in place of win32gui.
|
|
il = win32gui.ImageList_Create(
|
|
win32api.GetSystemMetrics(win32con.SM_CXSMICON),
|
|
win32api.GetSystemMetrics(win32con.SM_CYSMICON),
|
|
commctrl.ILC_COLOR32 | commctrl.ILC_MASK,
|
|
1, # initial size
|
|
0,
|
|
) # cGrow
|
|
|
|
shell_dll = os.path.join(win32api.GetSystemDirectory(), "shell32.dll")
|
|
large, small = win32gui.ExtractIconEx(shell_dll, 4, 1)
|
|
win32gui.ImageList_ReplaceIcon(il, -1, small[0])
|
|
win32gui.DestroyIcon(small[0])
|
|
win32gui.DestroyIcon(large[0])
|
|
win32gui.SendMessage(
|
|
self.hwndList, commctrl.LVM_SETIMAGELIST, commctrl.LVSIL_SMALL, il
|
|
)
|
|
|
|
# Setup the list control columns.
|
|
lvc = LVCOLUMN(
|
|
mask=commctrl.LVCF_FMT
|
|
| commctrl.LVCF_WIDTH
|
|
| commctrl.LVCF_TEXT
|
|
| commctrl.LVCF_SUBITEM
|
|
)
|
|
lvc.fmt = commctrl.LVCFMT_LEFT
|
|
lvc.iSubItem = 1
|
|
lvc.text = "Title"
|
|
lvc.cx = 200
|
|
win32gui.SendMessage(self.hwndList, commctrl.LVM_INSERTCOLUMN, 0, lvc.toparam())
|
|
lvc.iSubItem = 0
|
|
lvc.text = "Order"
|
|
lvc.cx = 50
|
|
win32gui.SendMessage(self.hwndList, commctrl.LVM_INSERTCOLUMN, 0, lvc.toparam())
|
|
|
|
win32gui.UpdateWindow(self.hwnd)
|
|
|
|
def ClearListItems(self):
|
|
win32gui.SendMessage(self.hwndList, commctrl.LVM_DELETEALLITEMS)
|
|
self.list_data = {}
|
|
|
|
def AddListItem(self, data, *columns):
|
|
num_items = win32gui.SendMessage(self.hwndList, commctrl.LVM_GETITEMCOUNT)
|
|
item = LVITEM(text=columns[0], iItem=num_items)
|
|
new_index = win32gui.SendMessage(
|
|
self.hwndList, commctrl.LVM_INSERTITEM, 0, item.toparam()
|
|
)
|
|
col_no = 1
|
|
for col in columns[1:]:
|
|
item = LVITEM(text=col, iItem=new_index, iSubItem=col_no)
|
|
win32gui.SendMessage(self.hwndList, commctrl.LVM_SETITEM, 0, item.toparam())
|
|
col_no += 1
|
|
self.list_data[new_index] = data
|
|
|
|
def OnInitDialog(self, hwnd, msg, wparam, lparam):
|
|
self.hwnd = hwnd
|
|
# centre the dialog
|
|
desktop = win32gui.GetDesktopWindow()
|
|
l, t, r, b = win32gui.GetWindowRect(self.hwnd)
|
|
dt_l, dt_t, dt_r, dt_b = win32gui.GetWindowRect(desktop)
|
|
centre_x, centre_y = win32gui.ClientToScreen(
|
|
desktop, ((dt_r - dt_l) // 2, (dt_b - dt_t) // 2)
|
|
)
|
|
win32gui.MoveWindow(
|
|
hwnd, centre_x - (r // 2), centre_y - (b // 2), r - l, b - t, 0
|
|
)
|
|
self._SetupList()
|
|
l, t, r, b = win32gui.GetClientRect(self.hwnd)
|
|
self._DoSize(r - l, b - t, 1)
|
|
|
|
def _DoSize(self, cx, cy, repaint=1):
|
|
# right-justify the textbox.
|
|
ctrl = win32gui.GetDlgItem(self.hwnd, IDC_SEARCHTEXT)
|
|
l, t, r, b = win32gui.GetWindowRect(ctrl)
|
|
l, t = win32gui.ScreenToClient(self.hwnd, (l, t))
|
|
r, b = win32gui.ScreenToClient(self.hwnd, (r, b))
|
|
win32gui.MoveWindow(ctrl, l, t, cx - l - 5, b - t, repaint)
|
|
# The button.
|
|
ctrl = win32gui.GetDlgItem(self.hwnd, IDC_BUTTON_DISPLAY)
|
|
l, t, r, b = win32gui.GetWindowRect(ctrl)
|
|
l, t = win32gui.ScreenToClient(self.hwnd, (l, t))
|
|
r, b = win32gui.ScreenToClient(self.hwnd, (r, b))
|
|
list_y = b + 10
|
|
w = r - l
|
|
win32gui.MoveWindow(ctrl, cx - 5 - w, t, w, b - t, repaint)
|
|
|
|
# The list control
|
|
win32gui.MoveWindow(self.hwndList, 0, list_y, cx, cy - list_y, repaint)
|
|
# The last column of the list control.
|
|
new_width = cx - win32gui.SendMessage(
|
|
self.hwndList, commctrl.LVM_GETCOLUMNWIDTH, 0
|
|
)
|
|
win32gui.SendMessage(self.hwndList, commctrl.LVM_SETCOLUMNWIDTH, 1, new_width)
|
|
|
|
def OnSize(self, hwnd, msg, wparam, lparam):
|
|
x = win32api.LOWORD(lparam)
|
|
y = win32api.HIWORD(lparam)
|
|
self._DoSize(x, y)
|
|
return 1
|
|
|
|
def OnSearchResult(self, hwnd, msg, wparam, lparam):
|
|
try:
|
|
while 1:
|
|
params = self.result_queue.get(0)
|
|
self.AddListItem(*params)
|
|
except queue.Empty:
|
|
pass
|
|
|
|
def OnSearchFinished(self, hwnd, msg, wparam, lparam):
|
|
print("OnSearchFinished")
|
|
|
|
def OnNotify(self, hwnd, msg, wparam, lparam):
|
|
info = win32gui_struct.UnpackNMITEMACTIVATE(lparam)
|
|
if info.code == commctrl.NM_DBLCLK:
|
|
print("Double click on item", info.iItem + 1)
|
|
return 1
|
|
|
|
def OnCommand(self, hwnd, msg, wparam, lparam):
|
|
id = win32api.LOWORD(wparam)
|
|
if id == IDC_BUTTON_SEARCH:
|
|
self.ClearListItems()
|
|
|
|
def fill_slowly(q, hwnd):
|
|
import time
|
|
|
|
for i in range(20):
|
|
q.put(("whatever", str(i + 1), "Search result " + str(i)))
|
|
win32gui.PostMessage(hwnd, WM_SEARCH_RESULT, 0, 0)
|
|
time.sleep(0.25)
|
|
win32gui.PostMessage(hwnd, WM_SEARCH_FINISHED, 0, 0)
|
|
|
|
import threading
|
|
|
|
self.result_queue = queue.Queue()
|
|
thread = threading.Thread(
|
|
target=fill_slowly, args=(self.result_queue, self.hwnd)
|
|
)
|
|
thread.start()
|
|
elif id == IDC_BUTTON_DISPLAY:
|
|
print("Display button selected")
|
|
sel = win32gui.SendMessage(
|
|
self.hwndList, commctrl.LVM_GETNEXTITEM, -1, commctrl.LVNI_SELECTED
|
|
)
|
|
print("The selected item is", sel + 1)
|
|
|
|
# These function differ based on how the window is used, so may be overridden
|
|
def OnClose(self, hwnd, msg, wparam, lparam):
|
|
raise NotImplementedError
|
|
|
|
def OnDestroy(self, hwnd, msg, wparam, lparam):
|
|
pass
|
|
|
|
|
|
# An implementation suitable for use with the Win32 Window functions (ie, not
|
|
# a true dialog)
|
|
class DemoWindow(DemoWindowBase):
|
|
def CreateWindow(self):
|
|
# Create the window via CreateDialogBoxIndirect - it can then
|
|
# work as a "normal" window, once a message loop is established.
|
|
self._DoCreate(win32gui.CreateDialogIndirect)
|
|
|
|
def OnClose(self, hwnd, msg, wparam, lparam):
|
|
win32gui.DestroyWindow(hwnd)
|
|
|
|
# We need to arrange to a WM_QUIT message to be sent to our
|
|
# PumpMessages() loop.
|
|
def OnDestroy(self, hwnd, msg, wparam, lparam):
|
|
win32gui.PostQuitMessage(0) # Terminate the app.
|
|
|
|
|
|
# An implementation suitable for use with the Win32 Dialog functions.
|
|
class DemoDialog(DemoWindowBase):
|
|
def DoModal(self):
|
|
return self._DoCreate(win32gui.DialogBoxIndirect)
|
|
|
|
def OnClose(self, hwnd, msg, wparam, lparam):
|
|
win32gui.EndDialog(hwnd, 0)
|
|
|
|
|
|
def DemoModal():
|
|
w = DemoDialog()
|
|
w.DoModal()
|
|
|
|
|
|
def DemoCreateWindow():
|
|
w = DemoWindow()
|
|
w.CreateWindow()
|
|
# PumpMessages runs until PostQuitMessage() is called by someone.
|
|
win32gui.PumpMessages()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
DemoModal()
|
|
DemoCreateWindow()
|