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.

win32rcparser.py 21KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. # Windows dialog .RC file parser, by Adam Walker.
  2. # This module was adapted from the spambayes project, and is Copyright
  3. # 2003/2004 The Python Software Foundation and is covered by the Python
  4. # Software Foundation license.
  5. """
  6. This is a parser for Windows .rc files, which are text files which define
  7. dialogs and other Windows UI resources.
  8. """
  9. __author__ = "Adam Walker"
  10. __version__ = "0.11"
  11. import os
  12. import pprint
  13. import shlex
  14. import stat
  15. import sys
  16. import commctrl
  17. import win32con
  18. _controlMap = {
  19. "DEFPUSHBUTTON": 0x80,
  20. "PUSHBUTTON": 0x80,
  21. "Button": 0x80,
  22. "GROUPBOX": 0x80,
  23. "Static": 0x82,
  24. "CTEXT": 0x82,
  25. "RTEXT": 0x82,
  26. "LTEXT": 0x82,
  27. "LISTBOX": 0x83,
  28. "SCROLLBAR": 0x84,
  29. "COMBOBOX": 0x85,
  30. "EDITTEXT": 0x81,
  31. "ICON": 0x82,
  32. "RICHEDIT": "RichEdit20A",
  33. }
  34. # These are "default styles" for certain controls - ie, Visual Studio assumes
  35. # the styles will be applied, and emits a "NOT {STYLE_NAME}" if it is to be
  36. # disabled. These defaults have been determined by experimentation, so may
  37. # not be completely accurate (most notably, some styles and/or control-types
  38. # may be missing.
  39. _addDefaults = {
  40. "EDITTEXT": win32con.WS_BORDER | win32con.WS_TABSTOP,
  41. "GROUPBOX": win32con.BS_GROUPBOX,
  42. "LTEXT": win32con.SS_LEFT,
  43. "DEFPUSHBUTTON": win32con.BS_DEFPUSHBUTTON | win32con.WS_TABSTOP,
  44. "PUSHBUTTON": win32con.WS_TABSTOP,
  45. "CTEXT": win32con.SS_CENTER,
  46. "RTEXT": win32con.SS_RIGHT,
  47. "ICON": win32con.SS_ICON,
  48. "LISTBOX": win32con.LBS_NOTIFY,
  49. }
  50. defaultControlStyle = win32con.WS_CHILD | win32con.WS_VISIBLE
  51. defaultControlStyleEx = 0
  52. class DialogDef:
  53. name = ""
  54. id = 0
  55. style = 0
  56. styleEx = None
  57. caption = ""
  58. font = "MS Sans Serif"
  59. fontSize = 8
  60. x = 0
  61. y = 0
  62. w = 0
  63. h = 0
  64. template = None
  65. def __init__(self, n, i):
  66. self.name = n
  67. self.id = i
  68. self.styles = []
  69. self.stylesEx = []
  70. self.controls = []
  71. # print "dialog def for ",self.name, self.id
  72. def createDialogTemplate(self):
  73. t = None
  74. self.template = [
  75. [
  76. self.caption,
  77. (self.x, self.y, self.w, self.h),
  78. self.style,
  79. self.styleEx,
  80. (self.fontSize, self.font),
  81. ]
  82. ]
  83. # Add the controls
  84. for control in self.controls:
  85. self.template.append(control.createDialogTemplate())
  86. return self.template
  87. class ControlDef:
  88. id = ""
  89. controlType = ""
  90. subType = ""
  91. idNum = 0
  92. style = defaultControlStyle
  93. styleEx = defaultControlStyleEx
  94. label = ""
  95. x = 0
  96. y = 0
  97. w = 0
  98. h = 0
  99. def __init__(self):
  100. self.styles = []
  101. self.stylesEx = []
  102. def toString(self):
  103. s = (
  104. "<Control id:"
  105. + self.id
  106. + " controlType:"
  107. + self.controlType
  108. + " subType:"
  109. + self.subType
  110. + " idNum:"
  111. + str(self.idNum)
  112. + " style:"
  113. + str(self.style)
  114. + " styles:"
  115. + str(self.styles)
  116. + " label:"
  117. + self.label
  118. + " x:"
  119. + str(self.x)
  120. + " y:"
  121. + str(self.y)
  122. + " w:"
  123. + str(self.w)
  124. + " h:"
  125. + str(self.h)
  126. + ">"
  127. )
  128. return s
  129. def createDialogTemplate(self):
  130. ct = self.controlType
  131. if "CONTROL" == ct:
  132. ct = self.subType
  133. if ct in _controlMap:
  134. ct = _controlMap[ct]
  135. t = [
  136. ct,
  137. self.label,
  138. self.idNum,
  139. (self.x, self.y, self.w, self.h),
  140. self.style,
  141. self.styleEx,
  142. ]
  143. # print t
  144. return t
  145. class StringDef:
  146. def __init__(self, id, idNum, value):
  147. self.id = id
  148. self.idNum = idNum
  149. self.value = value
  150. def __repr__(self):
  151. return "StringDef(%r, %r, %r)" % (self.id, self.idNum, self.value)
  152. class RCParser:
  153. next_id = 1001
  154. dialogs = {}
  155. _dialogs = {}
  156. debugEnabled = False
  157. token = ""
  158. def __init__(self):
  159. self.ungot = False
  160. self.ids = {"IDC_STATIC": -1}
  161. self.names = {-1: "IDC_STATIC"}
  162. self.bitmaps = {}
  163. self.stringTable = {}
  164. self.icons = {}
  165. def debug(self, *args):
  166. if self.debugEnabled:
  167. print(args)
  168. def getToken(self):
  169. if self.ungot:
  170. self.ungot = False
  171. self.debug("getToken returns (ungot):", self.token)
  172. return self.token
  173. self.token = self.lex.get_token()
  174. self.debug("getToken returns:", self.token)
  175. if self.token == "":
  176. self.token = None
  177. return self.token
  178. def ungetToken(self):
  179. self.ungot = True
  180. def getCheckToken(self, expected):
  181. tok = self.getToken()
  182. assert tok == expected, "Expected token '%s', but got token '%s'!" % (
  183. expected,
  184. tok,
  185. )
  186. return tok
  187. def getCommaToken(self):
  188. return self.getCheckToken(",")
  189. # Return the *current* token as a number, only consuming a token
  190. # if it is the negative-sign.
  191. def currentNumberToken(self):
  192. mult = 1
  193. if self.token == "-":
  194. mult = -1
  195. self.getToken()
  196. return int(self.token) * mult
  197. # Return the *current* token as a string literal (ie, self.token will be a
  198. # quote. consumes all tokens until the end of the string
  199. def currentQuotedString(self):
  200. # Handle quoted strings - pity shlex doesn't handle it.
  201. assert self.token.startswith('"'), self.token
  202. bits = [self.token]
  203. while 1:
  204. tok = self.getToken()
  205. if not tok.startswith('"'):
  206. self.ungetToken()
  207. break
  208. bits.append(tok)
  209. sval = "".join(bits)[1:-1] # Remove end quotes.
  210. # Fixup quotes in the body, and all (some?) quoted characters back
  211. # to their raw value.
  212. for i, o in ('""', '"'), ("\\r", "\r"), ("\\n", "\n"), ("\\t", "\t"):
  213. sval = sval.replace(i, o)
  214. return sval
  215. def load(self, rcstream):
  216. """
  217. RCParser.loadDialogs(rcFileName) -> None
  218. Load the dialog information into the parser. Dialog Definations can then be accessed
  219. using the "dialogs" dictionary member (name->DialogDef). The "ids" member contains the dictionary of id->name.
  220. The "names" member contains the dictionary of name->id
  221. """
  222. self.open(rcstream)
  223. self.getToken()
  224. while self.token != None:
  225. self.parse()
  226. self.getToken()
  227. def open(self, rcstream):
  228. self.lex = shlex.shlex(rcstream)
  229. self.lex.commenters = "//#"
  230. def parseH(self, file):
  231. lex = shlex.shlex(file)
  232. lex.commenters = "//"
  233. token = " "
  234. while token is not None:
  235. token = lex.get_token()
  236. if token == "" or token is None:
  237. token = None
  238. else:
  239. if token == "define":
  240. n = lex.get_token()
  241. i = int(lex.get_token())
  242. self.ids[n] = i
  243. if i in self.names:
  244. # Dupe ID really isn't a problem - most consumers
  245. # want to go from name->id, and this is OK.
  246. # It means you can't go from id->name though.
  247. pass
  248. # ignore AppStudio special ones
  249. # if not n.startswith("_APS_"):
  250. # print "Duplicate id",i,"for",n,"is", self.names[i]
  251. else:
  252. self.names[i] = n
  253. if self.next_id <= i:
  254. self.next_id = i + 1
  255. def parse(self):
  256. noid_parsers = {
  257. "STRINGTABLE": self.parse_stringtable,
  258. }
  259. id_parsers = {
  260. "DIALOG": self.parse_dialog,
  261. "DIALOGEX": self.parse_dialog,
  262. # "TEXTINCLUDE": self.parse_textinclude,
  263. "BITMAP": self.parse_bitmap,
  264. "ICON": self.parse_icon,
  265. }
  266. deep = 0
  267. base_token = self.token
  268. rp = noid_parsers.get(base_token)
  269. if rp is not None:
  270. rp()
  271. else:
  272. # Not something we parse that isn't prefixed by an ID
  273. # See if it is an ID prefixed item - if it is, our token
  274. # is the resource ID.
  275. resource_id = self.token
  276. self.getToken()
  277. if self.token is None:
  278. return
  279. if "BEGIN" == self.token:
  280. # A 'BEGIN' for a structure we don't understand - skip to the
  281. # matching 'END'
  282. deep = 1
  283. while deep != 0 and self.token is not None:
  284. self.getToken()
  285. self.debug("Zooming over", self.token)
  286. if "BEGIN" == self.token:
  287. deep += 1
  288. elif "END" == self.token:
  289. deep -= 1
  290. else:
  291. rp = id_parsers.get(self.token)
  292. if rp is not None:
  293. self.debug("Dispatching '%s'" % (self.token,))
  294. rp(resource_id)
  295. else:
  296. # We don't know what the resource type is, but we
  297. # have already consumed the next, which can cause problems,
  298. # so push it back.
  299. self.debug("Skipping top-level '%s'" % base_token)
  300. self.ungetToken()
  301. def addId(self, id_name):
  302. if id_name in self.ids:
  303. id = self.ids[id_name]
  304. else:
  305. # IDOK, IDCANCEL etc are special - if a real resource has this value
  306. for n in ["IDOK", "IDCANCEL", "IDYES", "IDNO", "IDABORT"]:
  307. if id_name == n:
  308. v = getattr(win32con, n)
  309. self.ids[n] = v
  310. self.names[v] = n
  311. return v
  312. id = self.next_id
  313. self.next_id += 1
  314. self.ids[id_name] = id
  315. self.names[id] = id_name
  316. return id
  317. def lang(self):
  318. while (
  319. self.token[0:4] == "LANG"
  320. or self.token[0:7] == "SUBLANG"
  321. or self.token == ","
  322. ):
  323. self.getToken()
  324. def parse_textinclude(self, res_id):
  325. while self.getToken() != "BEGIN":
  326. pass
  327. while 1:
  328. if self.token == "END":
  329. break
  330. s = self.getToken()
  331. def parse_stringtable(self):
  332. while self.getToken() != "BEGIN":
  333. pass
  334. while 1:
  335. self.getToken()
  336. if self.token == "END":
  337. break
  338. sid = self.token
  339. self.getToken()
  340. sd = StringDef(sid, self.addId(sid), self.currentQuotedString())
  341. self.stringTable[sid] = sd
  342. def parse_bitmap(self, name):
  343. return self.parse_bitmap_or_icon(name, self.bitmaps)
  344. def parse_icon(self, name):
  345. return self.parse_bitmap_or_icon(name, self.icons)
  346. def parse_bitmap_or_icon(self, name, dic):
  347. self.getToken()
  348. while not self.token.startswith('"'):
  349. self.getToken()
  350. bmf = self.token[1:-1] # quotes
  351. dic[name] = bmf
  352. def parse_dialog(self, name):
  353. dlg = DialogDef(name, self.addId(name))
  354. assert len(dlg.controls) == 0
  355. self._dialogs[name] = dlg
  356. extras = []
  357. self.getToken()
  358. while not self.token.isdigit():
  359. self.debug("extra", self.token)
  360. extras.append(self.token)
  361. self.getToken()
  362. dlg.x = int(self.token)
  363. self.getCommaToken()
  364. self.getToken() # number
  365. dlg.y = int(self.token)
  366. self.getCommaToken()
  367. self.getToken() # number
  368. dlg.w = int(self.token)
  369. self.getCommaToken()
  370. self.getToken() # number
  371. dlg.h = int(self.token)
  372. self.getToken()
  373. while not (self.token == None or self.token == "" or self.token == "END"):
  374. if self.token == "STYLE":
  375. self.dialogStyle(dlg)
  376. elif self.token == "EXSTYLE":
  377. self.dialogExStyle(dlg)
  378. elif self.token == "CAPTION":
  379. self.dialogCaption(dlg)
  380. elif self.token == "FONT":
  381. self.dialogFont(dlg)
  382. elif self.token == "BEGIN":
  383. self.controls(dlg)
  384. else:
  385. break
  386. self.dialogs[name] = dlg.createDialogTemplate()
  387. def dialogStyle(self, dlg):
  388. dlg.style, dlg.styles = self.styles([], win32con.DS_SETFONT)
  389. def dialogExStyle(self, dlg):
  390. self.getToken()
  391. dlg.styleEx, dlg.stylesEx = self.styles([], 0)
  392. def styles(self, defaults, defaultStyle):
  393. list = defaults
  394. style = defaultStyle
  395. if "STYLE" == self.token:
  396. self.getToken()
  397. i = 0
  398. Not = False
  399. while (
  400. (i % 2 == 1 and ("|" == self.token or "NOT" == self.token)) or (i % 2 == 0)
  401. ) and not self.token == None:
  402. Not = False
  403. if "NOT" == self.token:
  404. Not = True
  405. self.getToken()
  406. i += 1
  407. if self.token != "|":
  408. if self.token in win32con.__dict__:
  409. value = getattr(win32con, self.token)
  410. else:
  411. if self.token in commctrl.__dict__:
  412. value = getattr(commctrl, self.token)
  413. else:
  414. value = 0
  415. if Not:
  416. list.append("NOT " + self.token)
  417. self.debug("styles add Not", self.token, value)
  418. style &= ~value
  419. else:
  420. list.append(self.token)
  421. self.debug("styles add", self.token, value)
  422. style |= value
  423. self.getToken()
  424. self.debug("style is ", style)
  425. return style, list
  426. def dialogCaption(self, dlg):
  427. if "CAPTION" == self.token:
  428. self.getToken()
  429. self.token = self.token[1:-1]
  430. self.debug("Caption is:", self.token)
  431. dlg.caption = self.token
  432. self.getToken()
  433. def dialogFont(self, dlg):
  434. if "FONT" == self.token:
  435. self.getToken()
  436. dlg.fontSize = int(self.token)
  437. self.getCommaToken()
  438. self.getToken() # Font name
  439. dlg.font = self.token[1:-1] # it's quoted
  440. self.getToken()
  441. while "BEGIN" != self.token:
  442. self.getToken()
  443. def controls(self, dlg):
  444. if self.token == "BEGIN":
  445. self.getToken()
  446. # All controls look vaguely like:
  447. # TYPE [text, ] Control_id, l, t, r, b [, style]
  448. # .rc parser documents all control types as:
  449. # CHECKBOX, COMBOBOX, CONTROL, CTEXT, DEFPUSHBUTTON, EDITTEXT, GROUPBOX,
  450. # ICON, LISTBOX, LTEXT, PUSHBUTTON, RADIOBUTTON, RTEXT, SCROLLBAR
  451. without_text = ["EDITTEXT", "COMBOBOX", "LISTBOX", "SCROLLBAR"]
  452. while self.token != "END":
  453. control = ControlDef()
  454. control.controlType = self.token
  455. self.getToken()
  456. if control.controlType not in without_text:
  457. if self.token[0:1] == '"':
  458. control.label = self.currentQuotedString()
  459. # Some funny controls, like icons and picture controls use
  460. # the "window text" as extra resource ID (ie, the ID of the
  461. # icon itself). This may be either a literal, or an ID string.
  462. elif self.token == "-" or self.token.isdigit():
  463. control.label = str(self.currentNumberToken())
  464. else:
  465. # An ID - use the numeric equiv.
  466. control.label = str(self.addId(self.token))
  467. self.getCommaToken()
  468. self.getToken()
  469. # Control IDs may be "names" or literal ints
  470. if self.token == "-" or self.token.isdigit():
  471. control.id = self.currentNumberToken()
  472. control.idNum = control.id
  473. else:
  474. # name of an ID
  475. control.id = self.token
  476. control.idNum = self.addId(control.id)
  477. self.getCommaToken()
  478. if control.controlType == "CONTROL":
  479. self.getToken()
  480. control.subType = self.token[1:-1]
  481. thisDefaultStyle = defaultControlStyle | _addDefaults.get(
  482. control.subType, 0
  483. )
  484. # Styles
  485. self.getCommaToken()
  486. self.getToken()
  487. control.style, control.styles = self.styles([], thisDefaultStyle)
  488. else:
  489. thisDefaultStyle = defaultControlStyle | _addDefaults.get(
  490. control.controlType, 0
  491. )
  492. # incase no style is specified.
  493. control.style = thisDefaultStyle
  494. # Rect
  495. control.x = int(self.getToken())
  496. self.getCommaToken()
  497. control.y = int(self.getToken())
  498. self.getCommaToken()
  499. control.w = int(self.getToken())
  500. self.getCommaToken()
  501. self.getToken()
  502. control.h = int(self.token)
  503. self.getToken()
  504. if self.token == ",":
  505. self.getToken()
  506. control.style, control.styles = self.styles([], thisDefaultStyle)
  507. if self.token == ",":
  508. self.getToken()
  509. control.styleEx, control.stylesEx = self.styles(
  510. [], defaultControlStyleEx
  511. )
  512. # print control.toString()
  513. dlg.controls.append(control)
  514. def ParseStreams(rc_file, h_file):
  515. rcp = RCParser()
  516. if h_file:
  517. rcp.parseH(h_file)
  518. try:
  519. rcp.load(rc_file)
  520. except:
  521. lex = getattr(rcp, "lex", None)
  522. if lex:
  523. print("ERROR parsing dialogs at line", lex.lineno)
  524. print("Next 10 tokens are:")
  525. for i in range(10):
  526. print(lex.get_token(), end=" ")
  527. print()
  528. raise
  529. return rcp
  530. def Parse(rc_name, h_name=None):
  531. if h_name:
  532. h_file = open(h_name, "r")
  533. else:
  534. # See if same basename as the .rc
  535. h_name = rc_name[:-2] + "h"
  536. try:
  537. h_file = open(h_name, "r")
  538. except IOError:
  539. # See if MSVC default of 'resource.h' in the same dir.
  540. h_name = os.path.join(os.path.dirname(rc_name), "resource.h")
  541. try:
  542. h_file = open(h_name, "r")
  543. except IOError:
  544. # .h files are optional anyway
  545. h_file = None
  546. rc_file = open(rc_name, "r")
  547. try:
  548. return ParseStreams(rc_file, h_file)
  549. finally:
  550. if h_file is not None:
  551. h_file.close()
  552. rc_file.close()
  553. return rcp
  554. def GenerateFrozenResource(rc_name, output_name, h_name=None):
  555. """Converts an .rc windows resource source file into a python source file
  556. with the same basic public interface as the rest of this module.
  557. Particularly useful for py2exe or other 'freeze' type solutions,
  558. where a frozen .py file can be used inplace of a real .rc file.
  559. """
  560. rcp = Parse(rc_name, h_name)
  561. in_stat = os.stat(rc_name)
  562. out = open(output_name, "wt")
  563. out.write("#%s\n" % output_name)
  564. out.write("#This is a generated file. Please edit %s instead.\n" % rc_name)
  565. out.write("__version__=%r\n" % __version__)
  566. out.write(
  567. "_rc_size_=%d\n_rc_mtime_=%d\n"
  568. % (in_stat[stat.ST_SIZE], in_stat[stat.ST_MTIME])
  569. )
  570. out.write("class StringDef:\n")
  571. out.write("\tdef __init__(self, id, idNum, value):\n")
  572. out.write("\t\tself.id = id\n")
  573. out.write("\t\tself.idNum = idNum\n")
  574. out.write("\t\tself.value = value\n")
  575. out.write("\tdef __repr__(self):\n")
  576. out.write(
  577. '\t\treturn "StringDef(%r, %r, %r)" % (self.id, self.idNum, self.value)\n'
  578. )
  579. out.write("class FakeParser:\n")
  580. for name in "dialogs", "ids", "names", "bitmaps", "icons", "stringTable":
  581. out.write("\t%s = \\\n" % (name,))
  582. pprint.pprint(getattr(rcp, name), out)
  583. out.write("\n")
  584. out.write("def Parse(s):\n")
  585. out.write("\treturn FakeParser()\n")
  586. out.close()
  587. if __name__ == "__main__":
  588. if len(sys.argv) <= 1:
  589. print(__doc__)
  590. print()
  591. print("See test_win32rcparser.py, and the win32rcparser directory (both")
  592. print("in the test suite) for an example of this module's usage.")
  593. else:
  594. import pprint
  595. filename = sys.argv[1]
  596. if "-v" in sys.argv:
  597. RCParser.debugEnabled = 1
  598. print("Dumping all resources in '%s'" % filename)
  599. resources = Parse(filename)
  600. for id, ddef in resources.dialogs.items():
  601. print("Dialog %s (%d controls)" % (id, len(ddef)))
  602. pprint.pprint(ddef)
  603. print()
  604. for id, sdef in resources.stringTable.items():
  605. print("String %s=%r" % (id, sdef.value))
  606. print()
  607. for id, sdef in resources.bitmaps.items():
  608. print("Bitmap %s=%r" % (id, sdef))
  609. print()
  610. for id, sdef in resources.icons.items():
  611. print("Icon %s=%r" % (id, sdef))
  612. print()