win32gui_dialog.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. # A demo of a fairly complex dialog.
  2. #
  3. # Features:
  4. # * Uses a "dynamic dialog resource" to build the dialog.
  5. # * Uses a ListView control.
  6. # * Dynamically resizes content.
  7. # * Uses a second worker thread to fill the list.
  8. # * Demostrates support for windows XP themes.
  9. # If you are on Windows XP, and specify a '--noxp' argument, you will see:
  10. # * alpha-blend issues with icons
  11. # * The buttons are "old" style, rather than based on the XP theme.
  12. # Hence, using:
  13. # import winxpgui as win32gui
  14. # is recommened.
  15. # Please report any problems.
  16. import sys
  17. if "--noxp" in sys.argv:
  18. import win32gui
  19. else:
  20. import winxpgui as win32gui
  21. import win32gui_struct
  22. import win32api
  23. import win32con, winerror
  24. import struct, array
  25. import commctrl
  26. import queue
  27. import os
  28. IDC_SEARCHTEXT = 1024
  29. IDC_BUTTON_SEARCH = 1025
  30. IDC_BUTTON_DISPLAY = 1026
  31. IDC_LISTBOX = 1027
  32. WM_SEARCH_RESULT = win32con.WM_USER + 512
  33. WM_SEARCH_FINISHED = win32con.WM_USER + 513
  34. class _WIN32MASKEDSTRUCT:
  35. def __init__(self, **kw):
  36. full_fmt = ""
  37. for name, fmt, default, mask in self._struct_items_:
  38. self.__dict__[name] = None
  39. if fmt == "z":
  40. full_fmt += "pi"
  41. else:
  42. full_fmt += fmt
  43. for name, val in kw.items():
  44. if name not in self.__dict__:
  45. raise ValueError("LVITEM structures do not have an item '%s'" % (name,))
  46. self.__dict__[name] = val
  47. def __setattr__(self, attr, val):
  48. if not attr.startswith("_") and attr not in self.__dict__:
  49. raise AttributeError(attr)
  50. self.__dict__[attr] = val
  51. def toparam(self):
  52. self._buffs = []
  53. full_fmt = ""
  54. vals = []
  55. mask = 0
  56. # calc the mask
  57. for name, fmt, default, this_mask in self._struct_items_:
  58. if this_mask is not None and self.__dict__.get(name) is not None:
  59. mask |= this_mask
  60. self.mask = mask
  61. for name, fmt, default, this_mask in self._struct_items_:
  62. val = self.__dict__[name]
  63. if fmt == "z":
  64. fmt = "Pi"
  65. if val is None:
  66. vals.append(0)
  67. vals.append(0)
  68. else:
  69. # Note this demo still works with byte strings. An
  70. # alternate strategy would be to use unicode natively
  71. # and use the 'W' version of the messages - eg,
  72. # LVM_SETITEMW etc.
  73. val = val + "\0"
  74. if isinstance(val, str):
  75. val = val.encode("mbcs")
  76. str_buf = array.array("b", val)
  77. vals.append(str_buf.buffer_info()[0])
  78. vals.append(len(val))
  79. self._buffs.append(str_buf) # keep alive during the call.
  80. else:
  81. if val is None:
  82. val = default
  83. vals.append(val)
  84. full_fmt += fmt
  85. return struct.pack(*(full_fmt,) + tuple(vals))
  86. # NOTE: See the win32gui_struct module for an alternative way of dealing
  87. # with these structures
  88. class LVITEM(_WIN32MASKEDSTRUCT):
  89. _struct_items_ = [
  90. ("mask", "I", 0, None),
  91. ("iItem", "i", 0, None),
  92. ("iSubItem", "i", 0, None),
  93. ("state", "I", 0, commctrl.LVIF_STATE),
  94. ("stateMask", "I", 0, None),
  95. ("text", "z", None, commctrl.LVIF_TEXT),
  96. ("iImage", "i", 0, commctrl.LVIF_IMAGE),
  97. ("lParam", "i", 0, commctrl.LVIF_PARAM),
  98. ("iIdent", "i", 0, None),
  99. ]
  100. class LVCOLUMN(_WIN32MASKEDSTRUCT):
  101. _struct_items_ = [
  102. ("mask", "I", 0, None),
  103. ("fmt", "i", 0, commctrl.LVCF_FMT),
  104. ("cx", "i", 0, commctrl.LVCF_WIDTH),
  105. ("text", "z", None, commctrl.LVCF_TEXT),
  106. ("iSubItem", "i", 0, commctrl.LVCF_SUBITEM),
  107. ("iImage", "i", 0, commctrl.LVCF_IMAGE),
  108. ("iOrder", "i", 0, commctrl.LVCF_ORDER),
  109. ]
  110. class DemoWindowBase:
  111. def __init__(self):
  112. win32gui.InitCommonControls()
  113. self.hinst = win32gui.dllhandle
  114. self.list_data = {}
  115. def _RegisterWndClass(self):
  116. className = "PythonDocSearch"
  117. message_map = {}
  118. wc = win32gui.WNDCLASS()
  119. wc.SetDialogProc() # Make it a dialog class.
  120. wc.hInstance = self.hinst
  121. wc.lpszClassName = className
  122. wc.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW
  123. wc.hCursor = win32gui.LoadCursor( 0, win32con.IDC_ARROW )
  124. wc.hbrBackground = win32con.COLOR_WINDOW + 1
  125. wc.lpfnWndProc = message_map # could also specify a wndproc.
  126. # C code: wc.cbWndExtra = DLGWINDOWEXTRA + sizeof(HBRUSH) + (sizeof(COLORREF));
  127. wc.cbWndExtra = win32con.DLGWINDOWEXTRA + struct.calcsize("Pi")
  128. icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
  129. ## py.ico went away in python 2.5, load from executable instead
  130. this_app=win32api.GetModuleHandle(None)
  131. try:
  132. wc.hIcon=win32gui.LoadIcon(this_app, 1) ## python.exe and pythonw.exe
  133. except win32gui.error:
  134. wc.hIcon=win32gui.LoadIcon(this_app, 135) ## pythonwin's icon
  135. try:
  136. classAtom = win32gui.RegisterClass(wc)
  137. except win32gui.error as err_info:
  138. if err_info.winerror!=winerror.ERROR_CLASS_ALREADY_EXISTS:
  139. raise
  140. return className
  141. def _GetDialogTemplate(self, dlgClassName):
  142. style = win32con.WS_THICKFRAME | win32con.WS_POPUP | win32con.WS_VISIBLE | win32con.WS_CAPTION | win32con.WS_SYSMENU | win32con.DS_SETFONT | win32con.WS_MINIMIZEBOX
  143. cs = win32con.WS_CHILD | win32con.WS_VISIBLE
  144. title = "Dynamic Dialog Demo"
  145. # Window frame and title
  146. dlg = [ [title, (0, 0, 210, 250), style, None, (8, "MS Sans Serif"), None, dlgClassName], ]
  147. # ID label and text box
  148. dlg.append([130, "Enter something", -1, (5, 5, 200, 9), cs | win32con.SS_LEFT])
  149. s = cs | win32con.WS_TABSTOP | win32con.WS_BORDER
  150. dlg.append(['EDIT', None, IDC_SEARCHTEXT, (5, 15, 200, 12), s])
  151. # Search/Display Buttons
  152. # (x positions don't matter here)
  153. s = cs | win32con.WS_TABSTOP
  154. dlg.append([128, "Fill List", IDC_BUTTON_SEARCH, (5, 35, 50, 14), s | win32con.BS_DEFPUSHBUTTON])
  155. s = win32con.BS_PUSHBUTTON | s
  156. dlg.append([128, "Display", IDC_BUTTON_DISPLAY, (100, 35, 50, 14), s])
  157. # List control.
  158. # Can't make this work :(
  159. ## s = cs | win32con.WS_TABSTOP
  160. ## dlg.append(['SysListView32', "Title", IDC_LISTBOX, (5, 505, 200, 200), s])
  161. return dlg
  162. def _DoCreate(self, fn):
  163. message_map = {
  164. win32con.WM_SIZE: self.OnSize,
  165. win32con.WM_COMMAND: self.OnCommand,
  166. win32con.WM_NOTIFY: self.OnNotify,
  167. win32con.WM_INITDIALOG: self.OnInitDialog,
  168. win32con.WM_CLOSE: self.OnClose,
  169. win32con.WM_DESTROY: self.OnDestroy,
  170. WM_SEARCH_RESULT: self.OnSearchResult,
  171. WM_SEARCH_FINISHED: self.OnSearchFinished,
  172. }
  173. dlgClassName = self._RegisterWndClass()
  174. template = self._GetDialogTemplate(dlgClassName)
  175. return fn(self.hinst, template, 0, message_map)
  176. def _SetupList(self):
  177. child_style = win32con.WS_CHILD | win32con.WS_VISIBLE | win32con.WS_BORDER | win32con.WS_HSCROLL | win32con.WS_VSCROLL
  178. child_style |= commctrl.LVS_SINGLESEL | commctrl.LVS_SHOWSELALWAYS | commctrl.LVS_REPORT
  179. self.hwndList = win32gui.CreateWindow("SysListView32", None, child_style, 0, 0, 100, 100, self.hwnd, IDC_LISTBOX, self.hinst, None)
  180. child_ex_style = win32gui.SendMessage(self.hwndList, commctrl.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)
  181. child_ex_style |= commctrl.LVS_EX_FULLROWSELECT
  182. win32gui.SendMessage(self.hwndList, commctrl.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, child_ex_style)
  183. # Add an image list - use the builtin shell folder icon - this
  184. # demonstrates the problem with alpha-blending of icons on XP if
  185. # winxpgui is not used in place of win32gui.
  186. il = win32gui.ImageList_Create(
  187. win32api.GetSystemMetrics(win32con.SM_CXSMICON),
  188. win32api.GetSystemMetrics(win32con.SM_CYSMICON),
  189. commctrl.ILC_COLOR32 | commctrl.ILC_MASK,
  190. 1, # initial size
  191. 0) # cGrow
  192. shell_dll = os.path.join(win32api.GetSystemDirectory(), "shell32.dll")
  193. large, small = win32gui.ExtractIconEx(shell_dll, 4, 1)
  194. win32gui.ImageList_ReplaceIcon(il, -1, small[0])
  195. win32gui.DestroyIcon(small[0])
  196. win32gui.DestroyIcon(large[0])
  197. win32gui.SendMessage(self.hwndList, commctrl.LVM_SETIMAGELIST,
  198. commctrl.LVSIL_SMALL, il)
  199. # Setup the list control columns.
  200. lvc = LVCOLUMN(mask = commctrl.LVCF_FMT | commctrl.LVCF_WIDTH | commctrl.LVCF_TEXT | commctrl.LVCF_SUBITEM)
  201. lvc.fmt = commctrl.LVCFMT_LEFT
  202. lvc.iSubItem = 1
  203. lvc.text = "Title"
  204. lvc.cx = 200
  205. win32gui.SendMessage(self.hwndList, commctrl.LVM_INSERTCOLUMN, 0, lvc.toparam())
  206. lvc.iSubItem = 0
  207. lvc.text = "Order"
  208. lvc.cx = 50
  209. win32gui.SendMessage(self.hwndList, commctrl.LVM_INSERTCOLUMN, 0, lvc.toparam())
  210. win32gui.UpdateWindow(self.hwnd)
  211. def ClearListItems(self):
  212. win32gui.SendMessage(self.hwndList, commctrl.LVM_DELETEALLITEMS)
  213. self.list_data = {}
  214. def AddListItem(self, data, *columns):
  215. num_items = win32gui.SendMessage(self.hwndList, commctrl.LVM_GETITEMCOUNT)
  216. item = LVITEM(text=columns[0], iItem = num_items)
  217. new_index = win32gui.SendMessage(self.hwndList, commctrl.LVM_INSERTITEM, 0, item.toparam())
  218. col_no = 1
  219. for col in columns[1:]:
  220. item = LVITEM(text=col, iItem = new_index, iSubItem = col_no)
  221. win32gui.SendMessage(self.hwndList, commctrl.LVM_SETITEM, 0, item.toparam())
  222. col_no += 1
  223. self.list_data[new_index] = data
  224. def OnInitDialog(self, hwnd, msg, wparam, lparam):
  225. self.hwnd = hwnd
  226. # centre the dialog
  227. desktop = win32gui.GetDesktopWindow()
  228. l,t,r,b = win32gui.GetWindowRect(self.hwnd)
  229. dt_l, dt_t, dt_r, dt_b = win32gui.GetWindowRect(desktop)
  230. centre_x, centre_y = win32gui.ClientToScreen( desktop, ( (dt_r-dt_l)//2, (dt_b-dt_t)//2) )
  231. win32gui.MoveWindow(hwnd, centre_x-(r//2), centre_y-(b//2), r-l, b-t, 0)
  232. self._SetupList()
  233. l,t,r,b = win32gui.GetClientRect(self.hwnd)
  234. self._DoSize(r-l,b-t, 1)
  235. def _DoSize(self, cx, cy, repaint = 1):
  236. # right-justify the textbox.
  237. ctrl = win32gui.GetDlgItem(self.hwnd, IDC_SEARCHTEXT)
  238. l, t, r, b = win32gui.GetWindowRect(ctrl)
  239. l, t = win32gui.ScreenToClient(self.hwnd, (l,t) )
  240. r, b = win32gui.ScreenToClient(self.hwnd, (r,b) )
  241. win32gui.MoveWindow(ctrl, l, t, cx-l-5, b-t, repaint)
  242. # The button.
  243. ctrl = win32gui.GetDlgItem(self.hwnd, IDC_BUTTON_DISPLAY)
  244. l, t, r, b = win32gui.GetWindowRect(ctrl)
  245. l, t = win32gui.ScreenToClient(self.hwnd, (l,t) )
  246. r, b = win32gui.ScreenToClient(self.hwnd, (r,b) )
  247. list_y = b + 10
  248. w = r - l
  249. win32gui.MoveWindow(ctrl, cx - 5 - w, t, w, b-t, repaint)
  250. # The list control
  251. win32gui.MoveWindow(self.hwndList, 0, list_y, cx, cy-list_y, repaint)
  252. # The last column of the list control.
  253. new_width = cx - win32gui.SendMessage(self.hwndList, commctrl.LVM_GETCOLUMNWIDTH, 0)
  254. win32gui.SendMessage(self.hwndList, commctrl.LVM_SETCOLUMNWIDTH, 1, new_width)
  255. def OnSize(self, hwnd, msg, wparam, lparam):
  256. x = win32api.LOWORD(lparam)
  257. y = win32api.HIWORD(lparam)
  258. self._DoSize(x,y)
  259. return 1
  260. def OnSearchResult(self, hwnd, msg, wparam, lparam):
  261. try:
  262. while 1:
  263. params = self.result_queue.get(0)
  264. self.AddListItem(*params)
  265. except queue.Empty:
  266. pass
  267. def OnSearchFinished(self, hwnd, msg, wparam, lparam):
  268. print("OnSearchFinished")
  269. def OnNotify(self, hwnd, msg, wparam, lparam):
  270. info = win32gui_struct.UnpackNMITEMACTIVATE(lparam)
  271. if info.code == commctrl.NM_DBLCLK:
  272. print("Double click on item", info.iItem+1)
  273. return 1
  274. def OnCommand(self, hwnd, msg, wparam, lparam):
  275. id = win32api.LOWORD(wparam)
  276. if id == IDC_BUTTON_SEARCH:
  277. self.ClearListItems()
  278. def fill_slowly(q, hwnd):
  279. import time
  280. for i in range(20):
  281. q.put(("whatever", str(i+1), "Search result " + str(i) ))
  282. win32gui.PostMessage(hwnd, WM_SEARCH_RESULT, 0, 0)
  283. time.sleep(.25)
  284. win32gui.PostMessage(hwnd, WM_SEARCH_FINISHED, 0, 0)
  285. import threading
  286. self.result_queue = queue.Queue()
  287. thread = threading.Thread(target = fill_slowly, args=(self.result_queue, self.hwnd) )
  288. thread.start()
  289. elif id == IDC_BUTTON_DISPLAY:
  290. print("Display button selected")
  291. sel = win32gui.SendMessage(self.hwndList, commctrl.LVM_GETNEXTITEM, -1, commctrl.LVNI_SELECTED)
  292. print("The selected item is", sel+1)
  293. # These function differ based on how the window is used, so may be overridden
  294. def OnClose(self, hwnd, msg, wparam, lparam):
  295. raise NotImplementedError
  296. def OnDestroy(self, hwnd, msg, wparam, lparam):
  297. pass
  298. # An implementation suitable for use with the Win32 Window functions (ie, not
  299. # a true dialog)
  300. class DemoWindow(DemoWindowBase):
  301. def CreateWindow(self):
  302. # Create the window via CreateDialogBoxIndirect - it can then
  303. # work as a "normal" window, once a message loop is established.
  304. self._DoCreate(win32gui.CreateDialogIndirect)
  305. def OnClose(self, hwnd, msg, wparam, lparam):
  306. win32gui.DestroyWindow(hwnd)
  307. # We need to arrange to a WM_QUIT message to be sent to our
  308. # PumpMessages() loop.
  309. def OnDestroy(self, hwnd, msg, wparam, lparam):
  310. win32gui.PostQuitMessage(0) # Terminate the app.
  311. # An implementation suitable for use with the Win32 Dialog functions.
  312. class DemoDialog(DemoWindowBase):
  313. def DoModal(self):
  314. return self._DoCreate(win32gui.DialogBoxIndirect)
  315. def OnClose(self, hwnd, msg, wparam, lparam):
  316. win32gui.EndDialog(hwnd, 0)
  317. def DemoModal():
  318. w=DemoDialog()
  319. w.DoModal()
  320. def DemoCreateWindow():
  321. w=DemoWindow()
  322. w.CreateWindow()
  323. # PumpMessages runs until PostQuitMessage() is called by someone.
  324. win32gui.PumpMessages()
  325. if __name__=='__main__':
  326. DemoModal()
  327. DemoCreateWindow()