123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602 |
- # Windows dialog .RC file parser, by Adam Walker.
- # This module was adapted from the spambayes project, and is Copyright
- # 2003/2004 The Python Software Foundation and is covered by the Python
- # Software Foundation license.
- """
- This is a parser for Windows .rc files, which are text files which define
- dialogs and other Windows UI resources.
- """
- __author__="Adam Walker"
- __version__="0.11"
- import sys, os, shlex, stat
- import pprint
- import win32con
- import commctrl
- _controlMap = {"DEFPUSHBUTTON":0x80,
- "PUSHBUTTON":0x80,
- "Button":0x80,
- "GROUPBOX":0x80,
- "Static":0x82,
- "CTEXT":0x82,
- "RTEXT":0x82,
- "LTEXT":0x82,
- "LISTBOX":0x83,
- "SCROLLBAR":0x84,
- "COMBOBOX":0x85,
- "EDITTEXT":0x81,
- "ICON":0x82,
- "RICHEDIT":"RichEdit20A"
- }
- # These are "default styles" for certain controls - ie, Visual Studio assumes
- # the styles will be applied, and emits a "NOT {STYLE_NAME}" if it is to be
- # disabled. These defaults have been determined by experimentation, so may
- # not be completely accurate (most notably, some styles and/or control-types
- # may be missing.
- _addDefaults = {"EDITTEXT":win32con.WS_BORDER | win32con.WS_TABSTOP,
- "GROUPBOX":win32con.BS_GROUPBOX,
- "LTEXT":win32con.SS_LEFT,
- "DEFPUSHBUTTON":win32con.BS_DEFPUSHBUTTON | win32con.WS_TABSTOP,
- "PUSHBUTTON": win32con.WS_TABSTOP,
- "CTEXT":win32con.SS_CENTER,
- "RTEXT":win32con.SS_RIGHT,
- "ICON":win32con.SS_ICON,
- "LISTBOX":win32con.LBS_NOTIFY,
- }
- defaultControlStyle = win32con.WS_CHILD | win32con.WS_VISIBLE
- defaultControlStyleEx = 0
- class DialogDef:
- name = ""
- id = 0
- style = 0
- styleEx = None
- caption = ""
- font = "MS Sans Serif"
- fontSize = 8
- x = 0
- y = 0
- w = 0
- h = 0
- template = None
- def __init__(self, n, i):
- self.name = n
- self.id = i
- self.styles = []
- self.stylesEx = []
- self.controls = []
- #print "dialog def for ",self.name, self.id
- def createDialogTemplate(self):
- t = None
- self.template = [[self.caption,
- (self.x,self.y,self.w,self.h),
- self.style, self.styleEx,
- (self.fontSize, self.font)]
- ]
- # Add the controls
- for control in self.controls:
- self.template.append(control.createDialogTemplate())
- return self.template
- class ControlDef:
- id = ""
- controlType = ""
- subType = ""
- idNum = 0
- style = defaultControlStyle
- styleEx = defaultControlStyleEx
- label = ""
- x = 0
- y = 0
- w = 0
- h = 0
- def __init__(self):
- self.styles = []
- self.stylesEx = []
- def toString(self):
- s = "<Control id:"+self.id+" controlType:"+self.controlType+" subType:"+self.subType\
- +" idNum:"+str(self.idNum)+" style:"+str(self.style)+" styles:"+str(self.styles)+" label:"+self.label\
- +" x:"+str(self.x)+" y:"+str(self.y)+" w:"+str(self.w)+" h:"+str(self.h)+">"
- return s
- def createDialogTemplate(self):
- ct = self.controlType
- if "CONTROL"==ct:
- ct = self.subType
- if ct in _controlMap:
- ct = _controlMap[ct]
- t = [ct, self.label, self.idNum, (self.x, self.y, self.w, self.h), self.style, self.styleEx]
- #print t
- return t
- class StringDef:
- def __init__(self, id, idNum, value):
- self.id = id
- self.idNum = idNum
- self.value = value
- def __repr__(self):
- return "StringDef(%r, %r, %r)" % (self.id, self.idNum, self.value)
- class RCParser:
- next_id = 1001
- dialogs = {}
- _dialogs = {}
- debugEnabled = False
- token = ""
- def __init__(self):
- self.ungot = False
- self.ids = {"IDC_STATIC": -1}
- self.names = {-1:"IDC_STATIC"}
- self.bitmaps = {}
- self.stringTable = {}
- self.icons = {}
- def debug(self, *args):
- if self.debugEnabled:
- print(args)
- def getToken(self):
- if self.ungot:
- self.ungot = False
- self.debug("getToken returns (ungot):", self.token)
- return self.token
- self.token = self.lex.get_token()
- self.debug("getToken returns:", self.token)
- if self.token=="":
- self.token = None
- return self.token
- def ungetToken(self):
- self.ungot = True
- def getCheckToken(self, expected):
- tok = self.getToken()
- assert tok == expected, "Expected token '%s', but got token '%s'!" % (expected, tok)
- return tok
- def getCommaToken(self):
- return self.getCheckToken(",")
- # Return the *current* token as a number, only consuming a token
- # if it is the negative-sign.
- def currentNumberToken(self):
- mult = 1
- if self.token=='-':
- mult = -1
- self.getToken()
- return int(self.token) * mult
- # Return the *current* token as a string literal (ie, self.token will be a
- # quote. consumes all tokens until the end of the string
- def currentQuotedString(self):
- # Handle quoted strings - pity shlex doesn't handle it.
- assert self.token.startswith('"'), self.token
- bits = [self.token]
- while 1:
- tok = self.getToken()
- if not tok.startswith('"'):
- self.ungetToken()
- break
- bits.append(tok)
- sval = "".join(bits)[1:-1] # Remove end quotes.
- # Fixup quotes in the body, and all (some?) quoted characters back
- # to their raw value.
- for i, o in ('""', '"'), ("\\r", "\r"), ("\\n", "\n"), ("\\t", "\t"):
- sval = sval.replace(i, o)
- return sval
-
- def load(self, rcstream):
- """
- RCParser.loadDialogs(rcFileName) -> None
- Load the dialog information into the parser. Dialog Definations can then be accessed
- using the "dialogs" dictionary member (name->DialogDef). The "ids" member contains the dictionary of id->name.
- The "names" member contains the dictionary of name->id
- """
- self.open(rcstream)
- self.getToken()
- while self.token!=None:
- self.parse()
- self.getToken()
- def open(self, rcstream):
- self.lex = shlex.shlex(rcstream)
- self.lex.commenters = "//#"
- def parseH(self, file):
- lex = shlex.shlex(file)
- lex.commenters = "//"
- token = " "
- while token is not None:
- token = lex.get_token()
- if token == "" or token is None:
- token = None
- else:
- if token=='define':
- n = lex.get_token()
- i = int(lex.get_token())
- self.ids[n] = i
- if i in self.names:
- # Dupe ID really isn't a problem - most consumers
- # want to go from name->id, and this is OK.
- # It means you can't go from id->name though.
- pass
- # ignore AppStudio special ones
- #if not n.startswith("_APS_"):
- # print "Duplicate id",i,"for",n,"is", self.names[i]
- else:
- self.names[i] = n
- if self.next_id<=i:
- self.next_id = i+1
- def parse(self):
- noid_parsers = {
- "STRINGTABLE": self.parse_stringtable,
- }
- id_parsers = {
- "DIALOG" : self.parse_dialog,
- "DIALOGEX": self.parse_dialog,
- # "TEXTINCLUDE": self.parse_textinclude,
- "BITMAP": self.parse_bitmap,
- "ICON": self.parse_icon,
- }
- deep = 0
- base_token = self.token
- rp = noid_parsers.get(base_token)
- if rp is not None:
- rp()
- else:
- # Not something we parse that isn't prefixed by an ID
- # See if it is an ID prefixed item - if it is, our token
- # is the resource ID.
- resource_id = self.token
- self.getToken()
- if self.token is None:
- return
- if "BEGIN" == self.token:
- # A 'BEGIN' for a structure we don't understand - skip to the
- # matching 'END'
- deep = 1
- while deep!=0 and self.token is not None:
- self.getToken()
- self.debug("Zooming over", self.token)
- if "BEGIN" == self.token:
- deep += 1
- elif "END" == self.token:
- deep -= 1
- else:
- rp = id_parsers.get(self.token)
- if rp is not None:
- self.debug("Dispatching '%s'" % (self.token,))
- rp(resource_id)
- else:
- # We don't know what the resource type is, but we
- # have already consumed the next, which can cause problems,
- # so push it back.
- self.debug("Skipping top-level '%s'" % base_token)
- self.ungetToken()
- def addId(self, id_name):
- if id_name in self.ids:
- id = self.ids[id_name]
- else:
- # IDOK, IDCANCEL etc are special - if a real resource has this value
- for n in ["IDOK","IDCANCEL","IDYES","IDNO", "IDABORT"]:
- if id_name == n:
- v = getattr(win32con, n)
- self.ids[n] = v
- self.names[v] = n
- return v
- id = self.next_id
- self.next_id += 1
- self.ids[id_name] = id
- self.names[id] = id_name
- return id
- def lang(self):
- while self.token[0:4]=="LANG" or self.token[0:7]=="SUBLANG" or self.token==',':
- self.getToken();
- def parse_textinclude(self, res_id):
- while self.getToken() != "BEGIN":
- pass
- while 1:
- if self.token == "END":
- break
- s = self.getToken()
- def parse_stringtable(self):
- while self.getToken() != "BEGIN":
- pass
- while 1:
- self.getToken()
- if self.token == "END":
- break
- sid = self.token
- self.getToken()
- sd = StringDef(sid, self.addId(sid), self.currentQuotedString())
- self.stringTable[sid] = sd
- def parse_bitmap(self, name):
- return self.parse_bitmap_or_icon(name, self.bitmaps)
- def parse_icon(self, name):
- return self.parse_bitmap_or_icon(name, self.icons)
- def parse_bitmap_or_icon(self, name, dic):
- self.getToken()
- while not self.token.startswith('"'):
- self.getToken()
- bmf = self.token[1:-1] # quotes
- dic[name] = bmf
- def parse_dialog(self, name):
- dlg = DialogDef(name,self.addId(name))
- assert len(dlg.controls)==0
- self._dialogs[name] = dlg
- extras = []
- self.getToken()
- while not self.token.isdigit():
- self.debug("extra", self.token)
- extras.append(self.token)
- self.getToken()
- dlg.x = int(self.token)
- self.getCommaToken()
- self.getToken() # number
- dlg.y = int(self.token)
- self.getCommaToken()
- self.getToken() # number
- dlg.w = int(self.token)
- self.getCommaToken()
- self.getToken() # number
- dlg.h = int(self.token)
- self.getToken()
- while not (self.token==None or self.token=="" or self.token=="END"):
- if self.token=="STYLE":
- self.dialogStyle(dlg)
- elif self.token=="EXSTYLE":
- self.dialogExStyle(dlg)
- elif self.token=="CAPTION":
- self.dialogCaption(dlg)
- elif self.token=="FONT":
- self.dialogFont(dlg)
- elif self.token=="BEGIN":
- self.controls(dlg)
- else:
- break
- self.dialogs[name] = dlg.createDialogTemplate()
- def dialogStyle(self, dlg):
- dlg.style, dlg.styles = self.styles( [], win32con.DS_SETFONT)
- def dialogExStyle(self, dlg):
- self.getToken()
- dlg.styleEx, dlg.stylesEx = self.styles( [], 0)
- def styles(self, defaults, defaultStyle):
- list = defaults
- style = defaultStyle
- if "STYLE"==self.token:
- self.getToken()
- i = 0
- Not = False
- while ((i%2==1 and ("|"==self.token or "NOT"==self.token)) or (i%2==0)) and not self.token==None:
- Not = False;
- if "NOT"==self.token:
- Not = True
- self.getToken()
- i += 1
- if self.token!="|":
- if self.token in win32con.__dict__:
- value = getattr(win32con,self.token)
- else:
- if self.token in commctrl.__dict__:
- value = getattr(commctrl,self.token)
- else:
- value = 0
- if Not:
- list.append("NOT "+self.token)
- self.debug("styles add Not",self.token, value)
- style &= ~value
- else:
- list.append(self.token)
- self.debug("styles add", self.token, value)
- style |= value
- self.getToken()
- self.debug("style is ",style)
- return style, list
- def dialogCaption(self, dlg):
- if "CAPTION"==self.token:
- self.getToken()
- self.token = self.token[1:-1]
- self.debug("Caption is:",self.token)
- dlg.caption = self.token
- self.getToken()
- def dialogFont(self, dlg):
- if "FONT"==self.token:
- self.getToken()
- dlg.fontSize = int(self.token)
- self.getCommaToken()
- self.getToken() # Font name
- dlg.font = self.token[1:-1] # it's quoted
- self.getToken()
- while "BEGIN"!=self.token:
- self.getToken()
- def controls(self, dlg):
- if self.token=="BEGIN": self.getToken()
- # All controls look vaguely like:
- # TYPE [text, ] Control_id, l, t, r, b [, style]
- # .rc parser documents all control types as:
- # CHECKBOX, COMBOBOX, CONTROL, CTEXT, DEFPUSHBUTTON, EDITTEXT, GROUPBOX,
- # ICON, LISTBOX, LTEXT, PUSHBUTTON, RADIOBUTTON, RTEXT, SCROLLBAR
- without_text = ["EDITTEXT", "COMBOBOX", "LISTBOX", "SCROLLBAR"]
- while self.token!="END":
- control = ControlDef()
- control.controlType = self.token;
- self.getToken()
- if control.controlType not in without_text:
- if self.token[0:1]=='"':
- control.label = self.currentQuotedString()
- # Some funny controls, like icons and picture controls use
- # the "window text" as extra resource ID (ie, the ID of the
- # icon itself). This may be either a literal, or an ID string.
- elif self.token=="-" or self.token.isdigit():
- control.label = str(self.currentNumberToken())
- else:
- # An ID - use the numeric equiv.
- control.label = str(self.addId(self.token))
- self.getCommaToken()
- self.getToken()
- # Control IDs may be "names" or literal ints
- if self.token=="-" or self.token.isdigit():
- control.id = self.currentNumberToken()
- control.idNum = control.id
- else:
- # name of an ID
- control.id = self.token
- control.idNum = self.addId(control.id)
- self.getCommaToken()
- if control.controlType == "CONTROL":
- self.getToken()
- control.subType = self.token[1:-1]
- thisDefaultStyle = defaultControlStyle | \
- _addDefaults.get(control.subType, 0)
- # Styles
- self.getCommaToken()
- self.getToken()
- control.style, control.styles = self.styles([], thisDefaultStyle)
- else:
- thisDefaultStyle = defaultControlStyle | \
- _addDefaults.get(control.controlType, 0)
- # incase no style is specified.
- control.style = thisDefaultStyle
- # Rect
- control.x = int(self.getToken())
- self.getCommaToken()
- control.y = int(self.getToken())
- self.getCommaToken()
- control.w = int(self.getToken())
- self.getCommaToken()
- self.getToken()
- control.h = int(self.token)
- self.getToken()
- if self.token==",":
- self.getToken()
- control.style, control.styles = self.styles([], thisDefaultStyle)
- if self.token==",":
- self.getToken()
- control.styleEx, control.stylesEx = self.styles([], defaultControlStyleEx)
- #print control.toString()
- dlg.controls.append(control)
- def ParseStreams(rc_file, h_file):
- rcp = RCParser()
- if h_file:
- rcp.parseH(h_file)
- try:
- rcp.load(rc_file)
- except:
- lex = getattr(rcp, "lex", None)
- if lex:
- print("ERROR parsing dialogs at line", lex.lineno)
- print("Next 10 tokens are:")
- for i in range(10):
- print(lex.get_token(), end=' ')
- print()
- raise
- return rcp
-
- def Parse(rc_name, h_name = None):
- if h_name:
- h_file = open(h_name, "r")
- else:
- # See if same basename as the .rc
- h_name = rc_name[:-2]+"h"
- try:
- h_file = open(h_name, "r")
- except IOError:
- # See if MSVC default of 'resource.h' in the same dir.
- h_name = os.path.join(os.path.dirname(rc_name), "resource.h")
- try:
- h_file = open(h_name, "r")
- except IOError:
- # .h files are optional anyway
- h_file = None
- rc_file = open(rc_name, "r")
- try:
- return ParseStreams(rc_file, h_file)
- finally:
- if h_file is not None:
- h_file.close()
- rc_file.close()
- return rcp
- def GenerateFrozenResource(rc_name, output_name, h_name = None):
- """Converts an .rc windows resource source file into a python source file
- with the same basic public interface as the rest of this module.
- Particularly useful for py2exe or other 'freeze' type solutions,
- where a frozen .py file can be used inplace of a real .rc file.
- """
- rcp = Parse(rc_name, h_name)
- in_stat = os.stat(rc_name)
- out = open(output_name, "wt")
- out.write("#%s\n" % output_name)
- out.write("#This is a generated file. Please edit %s instead.\n" % rc_name)
- out.write("__version__=%r\n" % __version__)
- out.write("_rc_size_=%d\n_rc_mtime_=%d\n" % (in_stat[stat.ST_SIZE], in_stat[stat.ST_MTIME]))
- out.write("class StringDef:\n")
- out.write("\tdef __init__(self, id, idNum, value):\n")
- out.write("\t\tself.id = id\n")
- out.write("\t\tself.idNum = idNum\n")
- out.write("\t\tself.value = value\n")
- out.write("\tdef __repr__(self):\n")
- out.write("\t\treturn \"StringDef(%r, %r, %r)\" % (self.id, self.idNum, self.value)\n")
- out.write("class FakeParser:\n")
- for name in "dialogs", "ids", "names", "bitmaps", "icons", "stringTable":
- out.write("\t%s = \\\n" % (name,))
- pprint.pprint(getattr(rcp, name), out)
- out.write("\n")
- out.write("def Parse(s):\n")
- out.write("\treturn FakeParser()\n")
- out.close()
- if __name__=='__main__':
- if len(sys.argv) <= 1:
- print(__doc__)
- print()
- print("See test_win32rcparser.py, and the win32rcparser directory (both")
- print("in the test suite) for an example of this module's usage.")
- else:
- import pprint
- filename = sys.argv[1]
- if "-v" in sys.argv:
- RCParser.debugEnabled = 1
- print("Dumping all resources in '%s'" % filename)
- resources = Parse(filename)
- for id, ddef in resources.dialogs.items():
- print("Dialog %s (%d controls)" % (id, len(ddef)))
- pprint.pprint(ddef)
- print()
- for id, sdef in resources.stringTable.items():
- print("String %s=%r" % (id, sdef.value))
- print()
- for id, sdef in resources.bitmaps.items():
- print("Bitmap %s=%r" % (id, sdef))
- print()
- for id, sdef in resources.icons.items():
- print("Icon %s=%r" % (id, sdef))
- print()
|