win32rcparser.py 21 KB

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