edouard@3979: from __future__ import print_function edouard@3979: from __future__ import absolute_import edouard@3979: edouard@3979: import csv edouard@3979: import functools edouard@3979: from threading import Thread edouard@3984: from collections import OrderedDict edouard@3979: edouard@3979: import wx edouard@3979: import wx.dataview as dv edouard@3979: edouard@3995: import util.paths as paths edouard@3995: edouard@3984: # from perfect_hash import generate_code, IntSaltHash edouard@3979: edouard@3979: MQTT_IEC_types = dict( edouard@3979: # IEC61131| C type | sz edouard@3979: BOOL = ("uint8_t" , "X"), edouard@3979: SINT = ("int8_t" , "B"), edouard@3979: USINT = ("uint8_t" , "B"), edouard@3979: INT = ("int16_t" , "W"), edouard@3979: UINT = ("uint16_t", "W"), edouard@3979: DINT = ("uint32_t", "D"), edouard@3979: UDINT = ("int32_t" , "D"), edouard@3979: LINT = ("int64_t" , "L"), edouard@3979: ULINT = ("uint64_t", "L"), edouard@3979: REAL = ("float" , "D"), edouard@3979: LREAL = ("double" , "L"), edouard@3979: ) edouard@3979: edouard@3979: """ edouard@3979: QoS - Quality of Service edouard@3979: 0 : "At most one delivery" edouard@3979: 1 : "At least one delivery" edouard@3979: 2 : "Exactly one delivery" edouard@3979: """ edouard@3979: QoS_values = [0, 1, 2] edouard@3979: edouard@3980: def boolean(v): edouard@3980: if v in ["False","0"]: edouard@3980: return False edouard@3980: else: edouard@3980: return bool(v) edouard@3980: edouard@3988: _lstcolnames = [ "Topic", "QoS", "Retained", "Type", "Location"] edouard@3988: _lstcolwidths = [ 100, 50, 100, 100, 50] edouard@3988: _lstcoltypess = [ str, int, boolean, str, int] edouard@3988: _lstcoldeflts = [ "a/b/c", "1", False, "DINT", "0"] edouard@3988: edouard@3988: subsublist = lambda l : l[0:2] + l[3:5] edouard@3988: edouard@3988: lstcoldsc = { edouard@3988: "input" : type("",(),dict( edouard@3988: lstcolnames = subsublist(_lstcolnames), edouard@3988: lstcolwidths = subsublist(_lstcolwidths), edouard@3988: lstcoltypess = subsublist(_lstcoltypess), edouard@3988: lstcoldeflts = subsublist(_lstcoldeflts), edouard@3988: Location_column = 3)), edouard@3988: "output" : type("",(),dict( edouard@3988: lstcolnames = _lstcolnames, edouard@3988: lstcolwidths = _lstcolwidths, edouard@3988: lstcoltypess = _lstcoltypess, edouard@3988: lstcoldeflts = _lstcoldeflts, edouard@3988: Location_column = 4)), edouard@3988: } edouard@3979: edouard@3979: directions = ["input", "output"] edouard@3979: edouard@4005: # expected configuration entries with internal default value edouard@3979: authParams = { edouard@3979: "x509":[ edouard@4005: ("Verify", True), edouard@4005: ("KeyStore", None), edouard@4005: ("TrustStore", None)], edouard@3979: "UserPassword":[ edouard@3979: ("User", None), edouard@3979: ("Password", None)]} edouard@3979: edouard@3980: class MQTTTopicListModel(dv.PyDataViewIndexListModel): edouard@3988: def __init__(self, data, log, direction): edouard@3979: dv.PyDataViewIndexListModel.__init__(self, len(data)) edouard@3979: self.data = data edouard@3979: self.log = log edouard@3988: self.dsc = lstcoldsc[direction] edouard@3979: edouard@3979: def GetColumnType(self, col): edouard@3979: return "string" edouard@3979: edouard@3979: def GetValueByRow(self, row, col): edouard@3979: return str(self.data[row][col]) edouard@3979: edouard@3979: # This method is called when the user edits a data item in the view. edouard@3979: def SetValueByRow(self, value, row, col): edouard@3988: expectedtype = self.dsc.lstcoltypess[col] edouard@3979: edouard@3979: try: edouard@3979: v = expectedtype(value) edouard@3979: except ValueError: edouard@3979: self.log("String {} is invalid for type {}\n".format(value,expectedtype.__name__)) edouard@3979: return False edouard@3979: edouard@3988: if col == self.dsc.lstcolnames.index("QoS") and v not in QoS_values: edouard@3979: self.log("{} is invalid for IdType\n".format(value)) edouard@3979: return False edouard@3979: edouard@3999: line = self.data[row] edouard@3999: line[col] = v edouard@3999: self.data[row] = line edouard@3979: return True edouard@3979: edouard@3979: # Report how many columns this model provides data for. edouard@3979: def GetColumnCount(self): edouard@3988: return len(self.dsc.lstcolnames) edouard@3979: edouard@3979: # Report the number of rows in the model edouard@3979: def GetCount(self): edouard@3979: #self.log.write('GetCount') edouard@3979: return len(self.data) edouard@3979: edouard@3979: # Called to check if non-standard attributes should be used in the edouard@3979: # cell at (row, col) edouard@3979: def GetAttrByRow(self, row, col, attr): edouard@3988: if col == self.dsc.Location_column: edouard@3979: attr.SetColour('blue') edouard@3979: attr.SetBold(True) edouard@3979: return True edouard@3979: return False edouard@3979: edouard@3979: edouard@3979: def DeleteRows(self, rows): edouard@3979: # make a copy since we'll be sorting(mutating) the list edouard@3979: # use reverse order so the indexes don't change as we remove items edouard@3979: rows = sorted(rows, reverse=True) edouard@3979: edouard@3979: for row in rows: edouard@3979: # remove it from our data structure edouard@3979: del self.data[row] edouard@3979: # notify the view(s) using this model that it has been removed edouard@3979: self.RowDeleted(row) edouard@3979: edouard@3979: edouard@3979: def AddRow(self, value): edouard@3979: if self.data.append(value): edouard@3979: # notify views edouard@3979: self.RowAppended() edouard@3979: edouard@3979: def InsertDefaultRow(self, row): edouard@3988: self.data.insert(row, self.dsc.lstcoldeflts[:]) edouard@3979: # notify views edouard@3979: self.RowInserted(row) edouard@3979: edouard@3979: def ResetData(self): edouard@3979: self.Reset(len(self.data)) edouard@3979: edouard@3980: class MQTTTopicListPanel(wx.Panel): edouard@3979: def __init__(self, parent, log, model, direction): edouard@3979: self.log = log edouard@3979: wx.Panel.__init__(self, parent, -1) edouard@3979: edouard@3979: self.dvc = dv.DataViewCtrl(self, edouard@3979: style=wx.BORDER_THEME edouard@3979: | dv.DV_ROW_LINES edouard@3979: | dv.DV_HORIZ_RULES edouard@3979: | dv.DV_VERT_RULES edouard@3979: | dv.DV_MULTIPLE edouard@3979: ) edouard@3979: edouard@3979: self.model = model edouard@3979: edouard@3979: self.dvc.AssociateModel(self.model) edouard@3979: edouard@3988: dsc = lstcoldsc[direction] edouard@3988: for idx,(colname,width) in enumerate(zip(dsc.lstcolnames,dsc.lstcolwidths)): edouard@3979: self.dvc.AppendTextColumn(colname, idx, width=width, mode=dv.DATAVIEW_CELL_EDITABLE) edouard@3979: edouard@3979: edouard@3979: self.Sizer = wx.BoxSizer(wx.VERTICAL) edouard@3979: edouard@3979: self.direction = direction edouard@3979: titlestr = direction + " variables" edouard@3979: edouard@3979: title = wx.StaticText(self, label = titlestr) edouard@3979: edouard@3979: addbt = wx.Button(self, label="Add") edouard@3979: self.Bind(wx.EVT_BUTTON, self.OnAddRow, addbt) edouard@3979: delbt = wx.Button(self, label="Delete") edouard@3979: self.Bind(wx.EVT_BUTTON, self.OnDeleteRows, delbt) edouard@3979: edouard@3979: topsizer = wx.BoxSizer(wx.HORIZONTAL) edouard@3979: topsizer.Add(title, 1, wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT, 5) edouard@3979: topsizer.Add(addbt, 0, wx.LEFT|wx.RIGHT, 5) edouard@3979: topsizer.Add(delbt, 0, wx.LEFT|wx.RIGHT, 5) edouard@3979: self.Sizer.Add(topsizer, 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5) edouard@3979: self.Sizer.Add(self.dvc, 1, wx.EXPAND) edouard@3979: edouard@3979: edouard@3979: def OnAddRow(self, evt): edouard@3979: items = self.dvc.GetSelections() edouard@3979: row = self.model.GetRow(items[0]) if items else 0 edouard@3979: self.model.InsertDefaultRow(row) edouard@3979: edouard@3979: def OnDeleteRows(self, evt): edouard@3979: items = self.dvc.GetSelections() edouard@3979: rows = [self.model.GetRow(item) for item in items] edouard@3979: self.model.DeleteRows(rows) edouard@3979: edouard@3979: edouard@3992: class MQTTClientPanel(wx.SplitterWindow): edouard@3979: def __init__(self, parent, modeldata, log, config_getter): edouard@3979: self.log = log edouard@3992: wx.SplitterWindow.__init__(self, parent, style=wx.SUNKEN_BORDER | wx.SP_3D) edouard@3979: edouard@3979: self.config_getter = config_getter edouard@3979: edouard@3979: self.selected_datas = modeldata edouard@3988: self.selected_models = { direction:MQTTTopicListModel( edouard@3988: self.selected_datas[direction], log, direction) for direction in directions } edouard@3980: self.selected_lists = { direction:MQTTTopicListPanel( edouard@3992: self, log, edouard@3979: self.selected_models[direction], direction) edouard@3979: for direction in directions } edouard@3979: edouard@3992: self.SplitHorizontally(*[self.selected_lists[direction] for direction in directions]+[300]) edouard@3992: edouard@3979: self.SetAutoLayout(True) edouard@3979: edouard@3979: def OnClose(self): edouard@3979: pass edouard@3979: edouard@3979: def __del__(self): edouard@3979: self.OnClose() edouard@3979: edouard@3979: def Reset(self): edouard@3979: for direction in directions: edouard@3979: self.selected_models[direction].ResetData() edouard@3979: edouard@3979: edouard@3979: class MQTTClientList(list): edouard@3988: def __init__(self, log, change_callback, direction): edouard@3979: super(MQTTClientList, self).__init__(self) edouard@3979: self.log = log edouard@3979: self.change_callback = change_callback edouard@3988: self.dsc = lstcoldsc[direction] edouard@3979: edouard@3999: def _filter_line(self, value): edouard@3988: v = dict(list(zip(self.dsc.lstcolnames, value))) edouard@3979: edouard@3979: if type(v["Location"]) != int: edouard@3979: if len(self) == 0: edouard@3979: v["Location"] = 0 edouard@3979: else: edouard@3988: iecnums = set(zip(*self)[self.dsc.Location_column]) edouard@3979: greatest = max(iecnums) edouard@3979: holes = set(range(greatest)) - iecnums edouard@3979: v["Location"] = min(holes) if holes else greatest+1 edouard@3979: edouard@3979: try: edouard@3988: for t,n in zip(self.dsc.lstcoltypess, self.dsc.lstcolnames): edouard@3979: v[n] = t(v[n]) edouard@3979: except ValueError: edouard@3979: self.log("MQTT topic {} (Location={}) has invalid type\n".format(v["Topic"],v["Location"])) edouard@3999: return None edouard@3999: edouard@3999: if v["QoS"] not in QoS_values: edouard@3999: self.log("Unknown QoS\n".format(value)) edouard@3999: return None edouard@3979: edouard@3988: if len(self)>0 and v["Topic"] in list(zip(*self))[self.dsc.lstcolnames.index("Topic")]: edouard@3979: self.log("MQTT topic {} (Location={}) already in the list\n".format(v["Topic"],v["Location"])) edouard@3999: return None edouard@3999: edouard@3999: return [v[n] for n in self.dsc.lstcolnames] edouard@3999: edouard@3999: def insert(self, row, value): edouard@3999: v = self._filter_line(value) edouard@3999: if v is not None: edouard@3999: list.insert(self, row, v) edouard@3999: self.change_callback() edouard@3999: return True edouard@3999: return False edouard@3999: edouard@3999: def append(self, value): edouard@3999: v = self._filter_line(value) edouard@3999: if v is not None: edouard@3999: list.append(self, v) edouard@3999: self.change_callback() edouard@3999: return True edouard@3999: return False edouard@3999: edouard@3999: def __setitem__(self, index, value): edouard@3999: list.__setitem__(self, index, value) edouard@3979: self.change_callback() edouard@3979: edouard@3979: def __delitem__(self, index): edouard@3979: list.__delitem__(self, index) edouard@3979: self.change_callback() edouard@3979: edouard@3999: edouard@3979: class MQTTClientModel(dict): edouard@3979: def __init__(self, log, change_callback = lambda : None): edouard@3979: super(MQTTClientModel, self).__init__() edouard@3979: for direction in directions: edouard@3988: self[direction] = MQTTClientList(log, change_callback, direction) edouard@3979: edouard@3979: def LoadCSV(self,path): edouard@3979: with open(path, 'r') as csvfile: edouard@3979: reader = csv.reader(csvfile, delimiter=',', quotechar='"') edouard@3979: buf = {direction:[] for direction, _model in self.iteritems()} edouard@3979: for direction, model in self.iteritems(): edouard@3979: self[direction][:] = [] edouard@3979: for row in reader: edouard@3979: direction = row[0] edouard@3979: # avoids calling change callback when loading CSV edouard@3999: l = self[direction] edouard@3999: v = l._filter_line(row[1:]) edouard@3999: if v is not None: edouard@3999: list.append(l,v) edouard@3999: # TODO be verbose in case of malformed CSV edouard@3979: edouard@3979: def SaveCSV(self,path): edouard@3979: with open(path, 'w') as csvfile: edouard@3979: for direction, data in self.items(): edouard@3979: writer = csv.writer(csvfile, delimiter=',', edouard@3979: quotechar='"', quoting=csv.QUOTE_MINIMAL) edouard@3979: for row in data: edouard@3979: writer.writerow([direction] + row) edouard@3979: edouard@3979: def GenerateC(self, path, locstr, config): edouard@3995: c_template_filepath = paths.AbsNeighbourFile(__file__, "mqtt_template.c") edouard@3995: c_template_file = open(c_template_filepath , 'rb') edouard@3995: c_template = c_template_file.read() edouard@3995: c_template_file.close() edouard@3986: edouard@3979: formatdict = dict( edouard@3989: locstr = locstr, edouard@3989: uri = config["URI"], edouard@3989: clientID = config["clientID"], edouard@3989: decl = "", edouard@3989: topics = "", edouard@3989: cleanup = "", edouard@3989: init = "", edouard@3998: init_pubsub = "", edouard@3989: retrieve = "", edouard@3989: publish = "", edouard@3989: publish_changes = "" edouard@3979: ) edouard@3979: edouard@3986: edouard@3986: # Use Config's "MQTTVersion" to switch between protocol version at build time edouard@3986: if config["UseMQTT5"]: edouard@3986: formatdict["decl"] += """ edouard@3986: #define USE_MQTT_5""".format(**config) edouard@3986: edouard@3979: AuthType = config["AuthType"] edouard@4005: print(config) edouard@3979: if AuthType == "x509": edouard@4005: for k in ["KeyStore","TrustStore"]: edouard@4005: config[k] = '"'+config[k]+'"' if config[k] else "NULL" edouard@3979: formatdict["init"] += """ edouard@4005: INIT_x509({Verify:d}, {KeyStore}, {TrustStore})""".format(**config) edouard@4005: if AuthType == "PSK": edouard@4005: formatdict["init"] += """ edouard@4005: INIT_PSK("{Secret}", "{ID}")""".format(**config) edouard@3979: elif AuthType == "UserPassword": edouard@3979: formatdict["init"] += """ edouard@3979: INIT_UserPassword("{User}", "{Password}")""".format(**config) edouard@3979: else: edouard@3979: formatdict["init"] += """ edouard@3979: INIT_NoAuth()""" edouard@3979: edouard@3990: for row in self["output"]: edouard@3990: Topic, QoS, _Retained, iec_type, iec_number = row edouard@3990: Retained = 1 if _Retained=="True" else 0 edouard@3990: C_type, iec_size_prefix = MQTT_IEC_types[iec_type] edouard@3990: c_loc_name = "__Q" + iec_size_prefix + locstr + "_" + str(iec_number) edouard@3990: edouard@3990: formatdict["decl"] += """ edouard@3981: DECL_VAR({iec_type}, {C_type}, {c_loc_name})""".format(**locals()) edouard@3998: formatdict["init_pubsub"] += """ edouard@3990: INIT_PUBLICATION({Topic}, {QoS}, {C_type}, {c_loc_name}, {Retained})""".format(**locals()) edouard@3990: formatdict["publish"] += """ edouard@3990: WRITE_VALUE({c_loc_name}, {C_type})""".format(**locals()) edouard@3990: formatdict["publish_changes"] += """ edouard@3990: PUBLISH_CHANGE({Topic}, {QoS}, {C_type}, {c_loc_name}, {Retained})""".format(**locals()) edouard@3990: edouard@3990: # inputs need to be sorted for bisection search edouard@3990: for row in sorted(self["input"]): edouard@3990: Topic, QoS, iec_type, iec_number = row edouard@3990: C_type, iec_size_prefix = MQTT_IEC_types[iec_type] edouard@3990: c_loc_name = "__I" + iec_size_prefix + locstr + "_" + str(iec_number) edouard@3990: formatdict["decl"] += """ edouard@3990: DECL_VAR({iec_type}, {C_type}, {c_loc_name})""".format(**locals()) edouard@3990: formatdict["topics"] += """ edouard@3984: INIT_TOPIC({Topic}, {iec_type}, {c_loc_name})""".format(**locals()) edouard@3998: formatdict["init_pubsub"] += """ edouard@3984: INIT_SUBSCRIPTION({Topic}, {QoS})""".format(**locals()) edouard@3990: formatdict["retrieve"] += """ edouard@3990: READ_VALUE({c_loc_name}, {C_type})""".format(**locals()) edouard@3979: edouard@3995: Ccode = c_template.format(**formatdict) edouard@3986: edouard@3979: return Ccode edouard@3979: edouard@3979: if __name__ == "__main__": edouard@3979: edouard@3979: import wx.lib.mixins.inspection as wit edouard@3979: import sys,os edouard@3979: edouard@3979: app = wit.InspectableApp() edouard@3979: edouard@3979: frame = wx.Frame(None, -1, "MQTT Client Test App", size=(800,600)) edouard@3979: edouard@3979: argc = len(sys.argv) edouard@3979: edouard@3979: config={} edouard@3980: config["URI"] = sys.argv[1] if argc>1 else "tcp://localhost:1883" edouard@3980: config["clientID"] = sys.argv[2] if argc>2 else "" edouard@3979: config["AuthType"] = None edouard@3986: config["UseMQTT5"] = True edouard@3979: edouard@3980: if argc > 3: edouard@3980: AuthType = sys.argv[3] edouard@3979: config["AuthType"] = AuthType edouard@3980: for (name, default), value in zip_longest(authParams[AuthType], sys.argv[4:]): edouard@3979: if value is None: edouard@3979: if default is None: edouard@3979: raise Exception(name+" param expected") edouard@3979: value = default edouard@3979: config[name] = value edouard@3979: edouard@3979: test_panel = wx.Panel(frame) edouard@3979: test_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0) edouard@3979: test_sizer.AddGrowableCol(0) edouard@3979: test_sizer.AddGrowableRow(0) edouard@3979: edouard@3979: modeldata = MQTTClientModel(print) edouard@3979: edouard@3979: mqtttestpanel = MQTTClientPanel(test_panel, modeldata, print, lambda:config) edouard@3979: edouard@3979: def OnGenerate(evt): edouard@3979: dlg = wx.FileDialog( edouard@3979: frame, message="Generate file as ...", defaultDir=os.getcwd(), edouard@3986: defaultFile="", edouard@3979: wildcard="C (*.c)|*.c", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT edouard@3979: ) edouard@3979: edouard@3979: if dlg.ShowModal() == wx.ID_OK: edouard@3979: path = dlg.GetPath() edouard@3979: Ccode = """ edouard@3979: /* edouard@3979: In case open62541 was built just aside beremiz, you can build this test with: edouard@3979: gcc %s -o %s \\ edouard@3979: -I ../../open62541/plugins/include/ \\ edouard@3979: -I ../../open62541/build/src_generated/ \\ edouard@3979: -I ../../open62541/include/ \\ edouard@3979: -I ../../open62541/arch/ ../../open62541/build/bin/libopen62541.a edouard@3979: */ edouard@3979: edouard@3979: """%(path, path[:-2]) + modeldata.GenerateC(path, "test", config) + """ edouard@3979: edouard@3979: int LogMessage(uint8_t level, char* buf, uint32_t size){ edouard@3979: printf("log level:%d message:'%.*s'\\n", level, size, buf); edouard@3979: }; edouard@3979: edouard@3979: int main(int argc, char *argv[]) { edouard@3979: edouard@3979: __init_test(arc,argv); edouard@3986: edouard@3979: __retrieve_test(); edouard@3986: edouard@3979: __publish_test(); edouard@3979: edouard@3979: __cleanup_test(); edouard@3979: edouard@3979: return EXIT_SUCCESS; edouard@3979: } edouard@3979: """ edouard@3979: edouard@3979: with open(path, 'w') as Cfile: edouard@3979: Cfile.write(Ccode) edouard@3979: edouard@3979: edouard@3979: dlg.Destroy() edouard@3979: edouard@3979: def OnLoad(evt): edouard@3979: dlg = wx.FileDialog( edouard@3979: frame, message="Choose a file", edouard@3979: defaultDir=os.getcwd(), edouard@3979: defaultFile="", edouard@3979: wildcard="CSV (*.csv)|*.csv", edouard@3979: style=wx.FD_OPEN | wx.FD_CHANGE_DIR | wx.FD_FILE_MUST_EXIST ) edouard@3979: edouard@3979: if dlg.ShowModal() == wx.ID_OK: edouard@3979: path = dlg.GetPath() edouard@3979: modeldata.LoadCSV(path) edouard@3979: mqtttestpanel.Reset() edouard@3979: edouard@3979: dlg.Destroy() edouard@3979: edouard@3979: def OnSave(evt): edouard@3979: dlg = wx.FileDialog( edouard@3979: frame, message="Save file as ...", defaultDir=os.getcwd(), edouard@3986: defaultFile="", edouard@3979: wildcard="CSV (*.csv)|*.csv", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT edouard@3979: ) edouard@3979: edouard@3979: if dlg.ShowModal() == wx.ID_OK: edouard@3979: path = dlg.GetPath() edouard@3979: modeldata.SaveCSV(path) edouard@3979: edouard@3979: dlg.Destroy() edouard@3979: edouard@3979: test_sizer.Add(mqtttestpanel, flag=wx.GROW|wx.EXPAND) edouard@3979: edouard@3979: testbt_sizer = wx.BoxSizer(wx.HORIZONTAL) edouard@3979: edouard@3979: loadbt = wx.Button(test_panel, label="Load") edouard@3979: test_panel.Bind(wx.EVT_BUTTON, OnLoad, loadbt) edouard@3979: edouard@3979: savebt = wx.Button(test_panel, label="Save") edouard@3979: test_panel.Bind(wx.EVT_BUTTON, OnSave, savebt) edouard@3979: edouard@3979: genbt = wx.Button(test_panel, label="Generate") edouard@3979: test_panel.Bind(wx.EVT_BUTTON, OnGenerate, genbt) edouard@3979: edouard@3979: testbt_sizer.Add(loadbt, 0, wx.LEFT|wx.RIGHT, 5) edouard@3979: testbt_sizer.Add(savebt, 0, wx.LEFT|wx.RIGHT, 5) edouard@3979: testbt_sizer.Add(genbt, 0, wx.LEFT|wx.RIGHT, 5) edouard@3979: edouard@3979: test_sizer.Add(testbt_sizer, flag=wx.GROW) edouard@3979: test_sizer.Layout() edouard@3979: test_panel.SetAutoLayout(True) edouard@3979: test_panel.SetSizer(test_sizer) edouard@3979: edouard@3979: def OnClose(evt): edouard@3979: mqtttestpanel.OnClose() edouard@3979: evt.Skip() edouard@3979: edouard@3979: frame.Bind(wx.EVT_CLOSE, OnClose) edouard@3979: edouard@3979: frame.Show() edouard@3979: edouard@3979: app.MainLoop()