Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

win32gui_menu.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. # Demonstrates some advanced menu concepts using win32gui.
  2. # This creates a taskbar icon which has some fancy menus (but note that
  3. # selecting the menu items does nothing useful - see win32gui_taskbar.py
  4. # for examples of this.
  5. # NOTE: This is a work in progress. Todo:
  6. # * The "Checked" menu items don't work correctly - I'm not sure why.
  7. # * No support for GetMenuItemInfo.
  8. # Based on Andy McKay's demo code.
  9. from win32api import *
  10. # Try and use XP features, so we get alpha-blending etc.
  11. try:
  12. from winxpgui import *
  13. except ImportError:
  14. from win32gui import *
  15. import array
  16. import os
  17. import struct
  18. import sys
  19. import win32con
  20. from win32gui_struct import *
  21. this_dir = os.path.split(sys.argv[0])[0]
  22. class MainWindow:
  23. def __init__(self):
  24. message_map = {
  25. win32con.WM_DESTROY: self.OnDestroy,
  26. win32con.WM_COMMAND: self.OnCommand,
  27. win32con.WM_USER + 20: self.OnTaskbarNotify,
  28. # owner-draw related handlers.
  29. win32con.WM_MEASUREITEM: self.OnMeasureItem,
  30. win32con.WM_DRAWITEM: self.OnDrawItem,
  31. }
  32. # Register the Window class.
  33. wc = WNDCLASS()
  34. hinst = wc.hInstance = GetModuleHandle(None)
  35. wc.lpszClassName = "PythonTaskbarDemo"
  36. wc.lpfnWndProc = message_map # could also specify a wndproc.
  37. classAtom = RegisterClass(wc)
  38. # Create the Window.
  39. style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU
  40. self.hwnd = CreateWindow(
  41. classAtom,
  42. "Taskbar Demo",
  43. style,
  44. 0,
  45. 0,
  46. win32con.CW_USEDEFAULT,
  47. win32con.CW_USEDEFAULT,
  48. 0,
  49. 0,
  50. hinst,
  51. None,
  52. )
  53. UpdateWindow(self.hwnd)
  54. iconPathName = os.path.abspath(os.path.join(sys.prefix, "pyc.ico"))
  55. # py2.5 includes the .ico files in the DLLs dir for some reason.
  56. if not os.path.isfile(iconPathName):
  57. iconPathName = os.path.abspath(
  58. os.path.join(os.path.split(sys.executable)[0], "DLLs", "pyc.ico")
  59. )
  60. if not os.path.isfile(iconPathName):
  61. # Look in the source tree.
  62. iconPathName = os.path.abspath(
  63. os.path.join(os.path.split(sys.executable)[0], "..\\PC\\pyc.ico")
  64. )
  65. if os.path.isfile(iconPathName):
  66. icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
  67. hicon = LoadImage(
  68. hinst, iconPathName, win32con.IMAGE_ICON, 0, 0, icon_flags
  69. )
  70. else:
  71. iconPathName = None
  72. print("Can't find a Python icon file - using default")
  73. hicon = LoadIcon(0, win32con.IDI_APPLICATION)
  74. self.iconPathName = iconPathName
  75. # Load up some information about menus needed by our owner-draw code.
  76. # The font to use on the menu.
  77. ncm = SystemParametersInfo(win32con.SPI_GETNONCLIENTMETRICS)
  78. self.font_menu = CreateFontIndirect(ncm["lfMenuFont"])
  79. # spacing for our ownerdraw menus - not sure exactly what constants
  80. # should be used (and if you owner-draw all items on the menu, it
  81. # doesn't matter!)
  82. self.menu_icon_height = GetSystemMetrics(win32con.SM_CYMENU) - 4
  83. self.menu_icon_width = self.menu_icon_height
  84. self.icon_x_pad = 8 # space from end of icon to start of text.
  85. # A map we use to stash away data we need for ownerdraw. Keyed
  86. # by integer ID - that ID will be set in dwTypeData of the menu item.
  87. self.menu_item_map = {}
  88. # Finally, create the menu
  89. self.createMenu()
  90. flags = NIF_ICON | NIF_MESSAGE | NIF_TIP
  91. nid = (self.hwnd, 0, flags, win32con.WM_USER + 20, hicon, "Python Demo")
  92. Shell_NotifyIcon(NIM_ADD, nid)
  93. print("Please right-click on the Python icon in the taskbar")
  94. def createMenu(self):
  95. self.hmenu = menu = CreatePopupMenu()
  96. # Create our 'Exit' item with the standard, ugly 'close' icon.
  97. item, extras = PackMENUITEMINFO(
  98. text="Exit", hbmpItem=win32con.HBMMENU_MBAR_CLOSE, wID=1000
  99. )
  100. InsertMenuItem(menu, 0, 1, item)
  101. # Create a 'text only' menu via InsertMenuItem rather then
  102. # AppendMenu, just to prove we can!
  103. item, extras = PackMENUITEMINFO(text="Text only item", wID=1001)
  104. InsertMenuItem(menu, 0, 1, item)
  105. load_bmp_flags = win32con.LR_LOADFROMFILE | win32con.LR_LOADTRANSPARENT
  106. # These images are "over sized", so we load them scaled.
  107. hbmp = LoadImage(
  108. 0,
  109. os.path.join(this_dir, "images/smiley.bmp"),
  110. win32con.IMAGE_BITMAP,
  111. 20,
  112. 20,
  113. load_bmp_flags,
  114. )
  115. # Create a top-level menu with a bitmap
  116. item, extras = PackMENUITEMINFO(
  117. text="Menu with bitmap", hbmpItem=hbmp, wID=1002
  118. )
  119. InsertMenuItem(menu, 0, 1, item)
  120. # Owner-draw menus mainly from:
  121. # http://windowssdk.msdn.microsoft.com/en-us/library/ms647558.aspx
  122. # and:
  123. # http://www.codeguru.com/cpp/controls/menu/bitmappedmenus/article.php/c165
  124. # Create one with an icon - this is *lots* more work - we do it
  125. # owner-draw! The primary reason is to handle transparency better -
  126. # converting to a bitmap causes the background to be incorrect when
  127. # the menu item is selected. I can't see a simpler way.
  128. # First, load the icon we want to use.
  129. ico_x = GetSystemMetrics(win32con.SM_CXSMICON)
  130. ico_y = GetSystemMetrics(win32con.SM_CYSMICON)
  131. if self.iconPathName:
  132. hicon = LoadImage(
  133. 0,
  134. self.iconPathName,
  135. win32con.IMAGE_ICON,
  136. ico_x,
  137. ico_y,
  138. win32con.LR_LOADFROMFILE,
  139. )
  140. else:
  141. shell_dll = os.path.join(GetSystemDirectory(), "shell32.dll")
  142. large, small = win32gui.ExtractIconEx(shell_dll, 4, 1)
  143. hicon = small[0]
  144. DestroyIcon(large[0])
  145. # Stash away the text and hicon in our map, and add the owner-draw
  146. # item to the menu.
  147. index = 0
  148. self.menu_item_map[index] = (hicon, "Menu with owner-draw icon")
  149. item, extras = PackMENUITEMINFO(
  150. fType=win32con.MFT_OWNERDRAW, dwItemData=index, wID=1009
  151. )
  152. InsertMenuItem(menu, 0, 1, item)
  153. # Add another icon-based icon - but this time using HBMMENU_CALLBACK
  154. # in the hbmpItem elt, so we only need to draw the icon (ie, not the
  155. # text or checkmark)
  156. index = 1
  157. self.menu_item_map[index] = (hicon, None)
  158. item, extras = PackMENUITEMINFO(
  159. text="Menu with o-d icon 2",
  160. dwItemData=index,
  161. hbmpItem=win32con.HBMMENU_CALLBACK,
  162. wID=1010,
  163. )
  164. InsertMenuItem(menu, 0, 1, item)
  165. # Add another icon-based icon - this time by converting
  166. # via bitmap. Note the icon background when selected is ugly :(
  167. hdcBitmap = CreateCompatibleDC(0)
  168. hdcScreen = GetDC(0)
  169. hbm = CreateCompatibleBitmap(hdcScreen, ico_x, ico_y)
  170. hbmOld = SelectObject(hdcBitmap, hbm)
  171. SetBkMode(hdcBitmap, win32con.TRANSPARENT)
  172. # Fill the background.
  173. brush = GetSysColorBrush(win32con.COLOR_MENU)
  174. FillRect(hdcBitmap, (0, 0, 16, 16), brush)
  175. # unclear if brush needs to be freed. Best clue I can find is:
  176. # "GetSysColorBrush returns a cached brush instead of allocating a new
  177. # one." - implies no DeleteObject.
  178. # draw the icon
  179. DrawIconEx(hdcBitmap, 0, 0, hicon, ico_x, ico_y, 0, 0, win32con.DI_NORMAL)
  180. SelectObject(hdcBitmap, hbmOld)
  181. DeleteDC(hdcBitmap)
  182. item, extras = PackMENUITEMINFO(
  183. text="Menu with icon", hbmpItem=hbm.Detach(), wID=1011
  184. )
  185. InsertMenuItem(menu, 0, 1, item)
  186. # Create a sub-menu, and put a few funky ones there.
  187. self.sub_menu = sub_menu = CreatePopupMenu()
  188. # A 'checkbox' menu.
  189. item, extras = PackMENUITEMINFO(
  190. fState=win32con.MFS_CHECKED, text="Checkbox menu", hbmpItem=hbmp, wID=1003
  191. )
  192. InsertMenuItem(sub_menu, 0, 1, item)
  193. # A 'radio' menu.
  194. InsertMenu(sub_menu, 0, win32con.MF_BYPOSITION, win32con.MF_SEPARATOR, None)
  195. item, extras = PackMENUITEMINFO(
  196. fType=win32con.MFT_RADIOCHECK,
  197. fState=win32con.MFS_CHECKED,
  198. text="Checkbox menu - bullet 1",
  199. hbmpItem=hbmp,
  200. wID=1004,
  201. )
  202. InsertMenuItem(sub_menu, 0, 1, item)
  203. item, extras = PackMENUITEMINFO(
  204. fType=win32con.MFT_RADIOCHECK,
  205. fState=win32con.MFS_UNCHECKED,
  206. text="Checkbox menu - bullet 2",
  207. hbmpItem=hbmp,
  208. wID=1005,
  209. )
  210. InsertMenuItem(sub_menu, 0, 1, item)
  211. # And add the sub-menu to the top-level menu.
  212. item, extras = PackMENUITEMINFO(text="Sub-Menu", hSubMenu=sub_menu)
  213. InsertMenuItem(menu, 0, 1, item)
  214. # Set 'Exit' as the default option.
  215. SetMenuDefaultItem(menu, 1000, 0)
  216. def OnDestroy(self, hwnd, msg, wparam, lparam):
  217. nid = (self.hwnd, 0)
  218. Shell_NotifyIcon(NIM_DELETE, nid)
  219. PostQuitMessage(0) # Terminate the app.
  220. def OnTaskbarNotify(self, hwnd, msg, wparam, lparam):
  221. if lparam == win32con.WM_RBUTTONUP:
  222. print("You right clicked me.")
  223. # display the menu at the cursor pos.
  224. pos = GetCursorPos()
  225. SetForegroundWindow(self.hwnd)
  226. TrackPopupMenu(
  227. self.hmenu, win32con.TPM_LEFTALIGN, pos[0], pos[1], 0, self.hwnd, None
  228. )
  229. PostMessage(self.hwnd, win32con.WM_NULL, 0, 0)
  230. elif lparam == win32con.WM_LBUTTONDBLCLK:
  231. print("You double-clicked me")
  232. # find the default menu item and fire it.
  233. cmd = GetMenuDefaultItem(self.hmenu, False, 0)
  234. if cmd == -1:
  235. print("Can't find a default!")
  236. # and just pretend it came from the menu
  237. self.OnCommand(hwnd, win32con.WM_COMMAND, cmd, 0)
  238. return 1
  239. def OnCommand(self, hwnd, msg, wparam, lparam):
  240. id = LOWORD(wparam)
  241. if id == 1000:
  242. print("Goodbye")
  243. DestroyWindow(self.hwnd)
  244. elif id in (1003, 1004, 1005):
  245. # Our 'checkbox' and 'radio' items
  246. state = GetMenuState(self.sub_menu, id, win32con.MF_BYCOMMAND)
  247. if state == -1:
  248. raise RuntimeError("No item found")
  249. if state & win32con.MF_CHECKED:
  250. check_flags = win32con.MF_UNCHECKED
  251. print("Menu was checked - unchecking")
  252. else:
  253. check_flags = win32con.MF_CHECKED
  254. print("Menu was unchecked - checking")
  255. if id == 1003:
  256. # simple checkbox
  257. rc = CheckMenuItem(
  258. self.sub_menu, id, win32con.MF_BYCOMMAND | check_flags
  259. )
  260. else:
  261. # radio button - must pass the first and last IDs in the
  262. # "group", and the ID in the group that is to be selected.
  263. rc = CheckMenuRadioItem(
  264. self.sub_menu, 1004, 1005, id, win32con.MF_BYCOMMAND
  265. )
  266. # Get and check the new state - first the simple way...
  267. new_state = GetMenuState(self.sub_menu, id, win32con.MF_BYCOMMAND)
  268. if new_state & win32con.MF_CHECKED != check_flags:
  269. raise RuntimeError("The new item didn't get the new checked state!")
  270. # Now the long-winded way via GetMenuItemInfo...
  271. buf, extras = EmptyMENUITEMINFO()
  272. win32gui.GetMenuItemInfo(self.sub_menu, id, False, buf)
  273. (
  274. fType,
  275. fState,
  276. wID,
  277. hSubMenu,
  278. hbmpChecked,
  279. hbmpUnchecked,
  280. dwItemData,
  281. text,
  282. hbmpItem,
  283. ) = UnpackMENUITEMINFO(buf)
  284. if fState & win32con.MF_CHECKED != check_flags:
  285. raise RuntimeError("The new item didn't get the new checked state!")
  286. else:
  287. print("OnCommand for ID", id)
  288. # Owner-draw related functions. We only have 1 owner-draw item, but
  289. # we pretend we have more than that :)
  290. def OnMeasureItem(self, hwnd, msg, wparam, lparam):
  291. ## Last item of MEASUREITEMSTRUCT is a ULONG_PTR
  292. fmt = "5iP"
  293. buf = PyMakeBuffer(struct.calcsize(fmt), lparam)
  294. data = struct.unpack(fmt, buf)
  295. ctlType, ctlID, itemID, itemWidth, itemHeight, itemData = data
  296. hicon, text = self.menu_item_map[itemData]
  297. if text is None:
  298. # Only drawing icon due to HBMMENU_CALLBACK
  299. cx = self.menu_icon_width
  300. cy = self.menu_icon_height
  301. else:
  302. # drawing the lot!
  303. dc = GetDC(hwnd)
  304. oldFont = SelectObject(dc, self.font_menu)
  305. cx, cy = GetTextExtentPoint32(dc, text)
  306. SelectObject(dc, oldFont)
  307. ReleaseDC(hwnd, dc)
  308. cx += GetSystemMetrics(win32con.SM_CXMENUCHECK)
  309. cx += self.menu_icon_width + self.icon_x_pad
  310. cy = GetSystemMetrics(win32con.SM_CYMENU)
  311. new_data = struct.pack(fmt, ctlType, ctlID, itemID, cx, cy, itemData)
  312. PySetMemory(lparam, new_data)
  313. return True
  314. def OnDrawItem(self, hwnd, msg, wparam, lparam):
  315. ## lparam is a DRAWITEMSTRUCT
  316. fmt = "5i2P4iP"
  317. data = struct.unpack(fmt, PyGetMemory(lparam, struct.calcsize(fmt)))
  318. (
  319. ctlType,
  320. ctlID,
  321. itemID,
  322. itemAction,
  323. itemState,
  324. hwndItem,
  325. hDC,
  326. left,
  327. top,
  328. right,
  329. bot,
  330. itemData,
  331. ) = data
  332. rect = left, top, right, bot
  333. hicon, text = self.menu_item_map[itemData]
  334. if text is None:
  335. # This means the menu-item had HBMMENU_CALLBACK - so all we
  336. # draw is the icon. rect is the entire area we should use.
  337. DrawIconEx(
  338. hDC, left, top, hicon, right - left, bot - top, 0, 0, win32con.DI_NORMAL
  339. )
  340. else:
  341. # If the user has selected the item, use the selected
  342. # text and background colors to display the item.
  343. selected = itemState & win32con.ODS_SELECTED
  344. if selected:
  345. crText = SetTextColor(hDC, GetSysColor(win32con.COLOR_HIGHLIGHTTEXT))
  346. crBkgnd = SetBkColor(hDC, GetSysColor(win32con.COLOR_HIGHLIGHT))
  347. each_pad = self.icon_x_pad // 2
  348. x_icon = left + GetSystemMetrics(win32con.SM_CXMENUCHECK) + each_pad
  349. x_text = x_icon + self.menu_icon_width + each_pad
  350. # Draw text first, specifying a complete rect to fill - this sets
  351. # up the background (but overwrites anything else already there!)
  352. # Select the font, draw it, and restore the previous font.
  353. hfontOld = SelectObject(hDC, self.font_menu)
  354. ExtTextOut(hDC, x_text, top + 2, win32con.ETO_OPAQUE, rect, text)
  355. SelectObject(hDC, hfontOld)
  356. # Icon image next. Icons are transparent - no need to handle
  357. # selection specially.
  358. DrawIconEx(
  359. hDC,
  360. x_icon,
  361. top + 2,
  362. hicon,
  363. self.menu_icon_width,
  364. self.menu_icon_height,
  365. 0,
  366. 0,
  367. win32con.DI_NORMAL,
  368. )
  369. # Return the text and background colors to their
  370. # normal state (not selected).
  371. if selected:
  372. SetTextColor(hDC, crText)
  373. SetBkColor(hDC, crBkgnd)
  374. def main():
  375. w = MainWindow()
  376. PumpMessages()
  377. if __name__ == "__main__":
  378. main()