vclean.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. """
  2. @package modules.vclean
  3. @brief Dialog for interactive construction of vector cleaning
  4. operations
  5. Classes:
  6. - vclean::VectorCleaningFrame
  7. (C) 2010-2011 by the GRASS Development Team
  8. This program is free software under the GNU General Public License
  9. (>=v2). Read the file COPYING that comes with GRASS for details.
  10. @author Markus Metz
  11. """
  12. import os
  13. import wx
  14. import wx.lib.scrolledpanel as scrolled
  15. from core.gcmd import RunCommand, GError
  16. from core import globalvar
  17. from core.utils import _
  18. from gui_core.gselect import Select
  19. from core.settings import UserSettings
  20. from grass.script import core as grass
  21. from gui_core.wrap import Button, StaticText, StaticBox, \
  22. TextCtrl
  23. class VectorCleaningFrame(wx.Frame):
  24. def __init__(
  25. self, parent, id=wx.ID_ANY,
  26. title=_('Set up vector cleaning tools'),
  27. style=wx.DEFAULT_FRAME_STYLE | wx.RESIZE_BORDER, **kwargs):
  28. """
  29. Dialog for interactively defining vector cleaning tools
  30. """
  31. wx.Frame.__init__(self, parent, id, title, style=style, **kwargs)
  32. self.parent = parent # GMFrame
  33. if self.parent:
  34. self.log = self.parent.GetLogWindow()
  35. else:
  36. self.log = None
  37. # grass command
  38. self.cmd = 'v.clean'
  39. # statusbar
  40. self.CreateStatusBar()
  41. # icon
  42. self.SetIcon(
  43. wx.Icon(
  44. os.path.join(
  45. globalvar.ICONDIR,
  46. 'grass.ico'),
  47. wx.BITMAP_TYPE_ICO))
  48. self.panel = wx.Panel(parent=self, id=wx.ID_ANY)
  49. # input map to clean
  50. self.inmap = ''
  51. # cleaned output map
  52. self.outmap = ''
  53. self.ftype = ''
  54. # cleaning tools
  55. self.toolslines = {}
  56. self.tool_desc_list = [
  57. _('break lines/boundaries'),
  58. _('remove duplicates'),
  59. _('remove dangles'),
  60. _('change boundary dangles to lines'),
  61. _('remove bridges'),
  62. _('change bridges to lines'),
  63. _('snap lines/boundaries'),
  64. _('remove duplicate area centroids'),
  65. _('break polygons'),
  66. _('prune lines/boundaries'),
  67. _('remove small areas'),
  68. _('remove lines/boundaries of zero length'),
  69. _('remove small angles at nodes')
  70. ]
  71. self.tool_list = [
  72. 'break',
  73. 'rmdupl',
  74. 'rmdangle',
  75. 'chdangle',
  76. 'rmbridge',
  77. 'chbridge',
  78. 'snap',
  79. 'rmdac',
  80. 'bpol',
  81. 'prune',
  82. 'rmarea',
  83. 'rmline',
  84. 'rmsa'
  85. ]
  86. self.ftype = [
  87. 'point',
  88. 'line',
  89. 'boundary',
  90. 'centroid',
  91. 'area',
  92. 'face']
  93. self.n_ftypes = len(self.ftype)
  94. self.tools_string = ''
  95. self.thresh_string = ''
  96. self.ftype_string = ''
  97. self.SetStatusText(_("Set up vector cleaning tools"))
  98. self.elem = 'vector'
  99. self.ctlabel = _('Choose cleaning tools and set thresholds')
  100. # top controls
  101. self.inmaplabel = StaticText(parent=self.panel, id=wx.ID_ANY,
  102. label=_('Select input vector map:'))
  103. self.selectionInput = Select(parent=self.panel, id=wx.ID_ANY,
  104. size=globalvar.DIALOG_GSELECT_SIZE,
  105. type='vector')
  106. self.ftype_check = {}
  107. ftypeBox = StaticBox(parent=self.panel, id=wx.ID_ANY,
  108. label=_(' Feature type: '))
  109. self.ftypeSizer = wx.StaticBoxSizer(ftypeBox, wx.HORIZONTAL)
  110. self.outmaplabel = StaticText(parent=self.panel, id=wx.ID_ANY,
  111. label=_('Select output vector map:'))
  112. self.selectionOutput = Select(parent=self.panel, id=wx.ID_ANY,
  113. size=globalvar.DIALOG_GSELECT_SIZE,
  114. mapsets=[grass.gisenv()['MAPSET'], ],
  115. fullyQualified=False,
  116. type='vector')
  117. self.overwrite = wx.CheckBox(
  118. parent=self.panel, id=wx.ID_ANY,
  119. label=_('Allow output files to overwrite existing files'))
  120. self.overwrite.SetValue(
  121. UserSettings.Get(
  122. group='cmd',
  123. key='overwrite',
  124. subkey='enabled'))
  125. # cleaning tools
  126. self.ct_label = StaticText(parent=self.panel, id=wx.ID_ANY,
  127. label=self.ctlabel)
  128. self.ct_panel = self._toolsPanel()
  129. # buttons to manage cleaning tools
  130. self.btn_add = Button(parent=self.panel, id=wx.ID_ADD)
  131. self.btn_remove = Button(parent=self.panel, id=wx.ID_REMOVE)
  132. self.btn_moveup = Button(parent=self.panel, id=wx.ID_UP)
  133. self.btn_movedown = Button(parent=self.panel, id=wx.ID_DOWN)
  134. # add one tool as default
  135. self.AddTool()
  136. self.selected = -1
  137. # Buttons
  138. self.btn_close = Button(parent=self.panel, id=wx.ID_CLOSE)
  139. self.btn_run = Button(
  140. parent=self.panel,
  141. id=wx.ID_ANY,
  142. label=_("&Run"))
  143. self.btn_run.SetDefault()
  144. self.btn_clipboard = Button(parent=self.panel, id=wx.ID_COPY)
  145. self.btn_clipboard.SetToolTip(
  146. _("Copy the current command string to the clipboard (Ctrl+C)"))
  147. self.btn_help = Button(parent=self.panel, id=wx.ID_HELP)
  148. # bindings
  149. self.btn_close.Bind(wx.EVT_BUTTON, self.OnClose)
  150. self.btn_run.Bind(wx.EVT_BUTTON, self.OnCleaningRun)
  151. self.btn_clipboard.Bind(wx.EVT_BUTTON, self.OnCopy)
  152. self.btn_help.Bind(wx.EVT_BUTTON, self.OnHelp)
  153. self.btn_add.Bind(wx.EVT_BUTTON, self.OnAddTool)
  154. self.btn_remove.Bind(wx.EVT_BUTTON, self.OnClearTool)
  155. self.btn_moveup.Bind(wx.EVT_BUTTON, self.OnMoveToolUp)
  156. self.btn_movedown.Bind(wx.EVT_BUTTON, self.OnMoveToolDown)
  157. # layout
  158. self._layout()
  159. self.SetMinSize(self.GetBestSize())
  160. self.CentreOnScreen()
  161. def _layout(self):
  162. sizer = wx.BoxSizer(wx.VERTICAL)
  163. #
  164. # input output
  165. #
  166. inSizer = wx.GridBagSizer(hgap=5, vgap=5)
  167. inSizer.Add(
  168. self.inmaplabel,
  169. pos=(
  170. 0,
  171. 0),
  172. flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL | wx.EXPAND,
  173. border=1)
  174. inSizer.Add(
  175. self.selectionInput,
  176. pos=(
  177. 1,
  178. 0),
  179. flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL | wx.EXPAND,
  180. border=1)
  181. self.ftype_check = [
  182. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_('point')),
  183. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_('line')),
  184. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_('boundary')),
  185. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_('centroid')),
  186. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_('area')),
  187. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_('face'))
  188. ]
  189. typeoptSizer = wx.BoxSizer(wx.HORIZONTAL)
  190. for num in range(0, self.n_ftypes):
  191. type_box = self.ftype_check[num]
  192. if self.ftype[num] in ('point', 'line', 'area'):
  193. type_box.SetValue(True)
  194. typeoptSizer.Add(type_box, flag=wx.ALIGN_LEFT, border=1)
  195. self.ftypeSizer.Add(typeoptSizer,
  196. flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL, border=2)
  197. outSizer = wx.GridBagSizer(hgap=5, vgap=5)
  198. outSizer.Add(
  199. self.outmaplabel,
  200. pos=(
  201. 0,
  202. 0),
  203. flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL | wx.EXPAND,
  204. border=1)
  205. outSizer.Add(
  206. self.selectionOutput,
  207. pos=(
  208. 1,
  209. 0),
  210. flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL | wx.EXPAND,
  211. border=1)
  212. replaceSizer = wx.BoxSizer(wx.HORIZONTAL)
  213. replaceSizer.Add(self.overwrite, proportion=1,
  214. flag=wx.ALL | wx.EXPAND, border=1)
  215. outSizer.Add(replaceSizer, pos=(2, 0),
  216. flag=wx.ALL | wx.EXPAND, border=1)
  217. #
  218. # tools selection
  219. #
  220. bodySizer = wx.GridBagSizer(hgap=5, vgap=5)
  221. bodySizer.Add(self.ct_label, pos=(0, 0), span=(1, 2),
  222. flag=wx.ALL, border=5)
  223. bodySizer.Add(self.ct_panel, pos=(1, 0), span=(1, 2))
  224. manageBoxSizer = wx.GridBagSizer(hgap=10, vgap=1)
  225. # start with row 1 for nicer layout
  226. manageBoxSizer.Add(
  227. self.btn_add, pos=(1, 0),
  228. border=2, flag=wx.ALL | wx.EXPAND)
  229. manageBoxSizer.Add(
  230. self.btn_remove, pos=(2, 0),
  231. border=2, flag=wx.ALL | wx.EXPAND)
  232. manageBoxSizer.Add(
  233. self.btn_moveup, pos=(3, 0),
  234. border=2, flag=wx.ALL | wx.EXPAND)
  235. manageBoxSizer.Add(
  236. self.btn_movedown, pos=(4, 0),
  237. border=2, flag=wx.ALL | wx.EXPAND)
  238. bodySizer.Add(manageBoxSizer, pos=(1, 2),
  239. flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=5)
  240. bodySizer.AddGrowableCol(2)
  241. #
  242. # standard buttons
  243. #
  244. btnSizer = wx.BoxSizer(wx.HORIZONTAL)
  245. btnSizer.Add(self.btn_close,
  246. flag=wx.LEFT | wx.RIGHT, border=5)
  247. btnSizer.Add(self.btn_run,
  248. flag=wx.LEFT | wx.RIGHT, border=5)
  249. btnSizer.Add(self.btn_clipboard,
  250. flag=wx.LEFT | wx.RIGHT, border=5)
  251. btnSizer.Add(self.btn_help,
  252. flag=wx.LEFT | wx.RIGHT, border=5)
  253. #
  254. # put it all together
  255. #
  256. sizer.Add(inSizer, proportion=0,
  257. flag=wx.ALL | wx.EXPAND, border=5)
  258. sizer.Add(self.ftypeSizer, proportion=0,
  259. flag=wx.ALL | wx.EXPAND, border=5)
  260. sizer.Add(outSizer, proportion=0,
  261. flag=wx.ALL | wx.EXPAND, border=5)
  262. sizer.Add(wx.StaticLine(parent=self, id=wx.ID_ANY,
  263. style=wx.LI_HORIZONTAL), proportion=0,
  264. flag=wx.EXPAND | wx.ALL, border=5)
  265. sizer.Add(bodySizer, proportion=1,
  266. flag=wx.ALL | wx.EXPAND, border=5)
  267. sizer.Add(wx.StaticLine(parent=self, id=wx.ID_ANY,
  268. style=wx.LI_HORIZONTAL), proportion=0,
  269. flag=wx.EXPAND | wx.ALL, border=5)
  270. sizer.Add(btnSizer, proportion=0,
  271. flag=wx.ALL | wx.ALIGN_RIGHT, border=5)
  272. self.panel.SetAutoLayout(True)
  273. self.panel.SetSizer(sizer)
  274. sizer.Fit(self.panel)
  275. self.Layout()
  276. def _toolsPanel(self):
  277. ct_panel = scrolled.ScrolledPanel(parent=self.panel, id=wx.ID_ANY,
  278. size=(500, 240),
  279. style=wx.SUNKEN_BORDER)
  280. self.ct_sizer = wx.GridBagSizer(vgap=2, hgap=4)
  281. ct_panel.SetSizer(self.ct_sizer)
  282. ct_panel.SetAutoLayout(True)
  283. return ct_panel
  284. def OnAddTool(self, event):
  285. """Add tool button pressed"""
  286. self.AddTool()
  287. def AddTool(self):
  288. snum = len(self.toolslines.keys())
  289. num = snum + 1
  290. # tool
  291. tool_cbox = wx.ComboBox(parent=self.ct_panel, id=1000 + num,
  292. size=(300, -1), choices=self.tool_desc_list,
  293. style=wx.CB_DROPDOWN |
  294. wx.CB_READONLY | wx.TE_PROCESS_ENTER)
  295. self.Bind(wx.EVT_COMBOBOX, self.OnSetTool, tool_cbox)
  296. # threshold
  297. txt_ctrl = TextCtrl(
  298. parent=self.ct_panel, id=2000 + num, value='0.00', size=(100, -1),
  299. style=wx.TE_NOHIDESEL)
  300. self.Bind(wx.EVT_TEXT, self.OnThreshValue, txt_ctrl)
  301. # select with tool number
  302. select = wx.CheckBox(
  303. parent=self.ct_panel,
  304. id=num,
  305. label=str(num) + '.')
  306. select.SetValue(False)
  307. self.Bind(wx.EVT_CHECKBOX, self.OnSelect, select)
  308. # start with row 1 and col 1 for nicer layout
  309. self.ct_sizer.Add(select, pos=(num, 1),
  310. flag=wx.ALIGN_CENTER | wx.RIGHT)
  311. self.ct_sizer.Add(tool_cbox, pos=(num, 2),
  312. flag=wx.ALIGN_CENTER | wx.RIGHT, border=5)
  313. self.ct_sizer.Add(txt_ctrl, pos=(num, 3),
  314. flag=wx.ALIGN_CENTER | wx.RIGHT, border=5)
  315. self.toolslines[num] = {'tool_desc': '',
  316. 'tool': '',
  317. 'thresh': '0.00'}
  318. self.ct_panel.Layout()
  319. self.ct_panel.SetupScrolling()
  320. def OnClearTool(self, event):
  321. """Remove tool button pressed"""
  322. id = self.selected
  323. if id > 0:
  324. self.FindWindowById(id + 1000).SetValue('')
  325. self.toolslines[id]['tool_desc'] = ''
  326. self.toolslines[id]['tool'] = ''
  327. self.SetStatusText(
  328. _("%s. cleaning tool removed, will be ignored") %
  329. id)
  330. else:
  331. self.SetStatusText(_("Please select a cleaning tool to remove"))
  332. def OnMoveToolUp(self, event):
  333. """Move up tool button pressed"""
  334. id = self.selected
  335. if id > 1:
  336. id_up = id - 1
  337. this_toolline = self.toolslines[id]
  338. up_toolline = self.toolslines[id_up]
  339. self.FindWindowById(id_up).SetValue(True)
  340. self.FindWindowById(
  341. id_up +
  342. 1000).SetValue(
  343. this_toolline['tool_desc'])
  344. self.FindWindowById(id_up + 2000).SetValue(this_toolline['thresh'])
  345. self.toolslines[id_up] = this_toolline
  346. self.FindWindowById(id).SetValue(False)
  347. self.FindWindowById(id + 1000).SetValue(up_toolline['tool_desc'])
  348. self.FindWindowById(id + 2000).SetValue(up_toolline['thresh'])
  349. self.toolslines[id] = up_toolline
  350. self.selected = id_up
  351. self.SetStatusText(_("%s. cleaning tool moved up") % id)
  352. elif id == 1:
  353. self.SetStatusText(_("1. cleaning tool can not be moved up "))
  354. elif id == -1:
  355. self.SetStatusText(_("Please select a cleaning tool to move up"))
  356. def OnMoveToolDown(self, event):
  357. """Move down tool button pressed"""
  358. id = self.selected
  359. snum = len(self.toolslines.keys())
  360. if id > 0 and id < snum:
  361. id_down = id + 1
  362. this_toolline = self.toolslines[id]
  363. down_toolline = self.toolslines[id_down]
  364. self.FindWindowById(id_down).SetValue(True)
  365. self.FindWindowById(
  366. id_down +
  367. 1000).SetValue(
  368. this_toolline['tool_desc'])
  369. self.FindWindowById(
  370. id_down +
  371. 2000).SetValue(
  372. this_toolline['thresh'])
  373. self.toolslines[id_down] = this_toolline
  374. self.FindWindowById(id).SetValue(False)
  375. self.FindWindowById(id + 1000).SetValue(down_toolline['tool_desc'])
  376. self.FindWindowById(id + 2000).SetValue(down_toolline['thresh'])
  377. self.toolslines[id] = down_toolline
  378. self.selected = id_down
  379. self.SetStatusText(_("%s. cleaning tool moved down") % id)
  380. elif id == snum:
  381. self.SetStatusText(_("Last cleaning tool can not be moved down "))
  382. elif id == -1:
  383. self.SetStatusText(_("Please select a cleaning tool to move down"))
  384. def OnSetTool(self, event):
  385. """Tool was defined"""
  386. id = event.GetId()
  387. tool_no = id - 1000
  388. num = self.FindWindowById(id).GetCurrentSelection()
  389. self.toolslines[tool_no]['tool_desc'] = self.tool_desc_list[num]
  390. self.toolslines[tool_no]['tool'] = self.tool_list[num]
  391. self.SetStatusText(
  392. str(tool_no) +
  393. '. ' +
  394. _("cleaning tool: '%s'") %
  395. (self.tool_list[num]))
  396. def OnThreshValue(self, event):
  397. """Threshold value was entered"""
  398. id = event.GetId()
  399. num = id - 2000
  400. self.toolslines[num]['thresh'] = self.FindWindowById(id).GetValue()
  401. self.SetStatusText(
  402. _("Threshold for %(num)s. tool '%(tool)s': %(thresh)s") % {
  403. 'num': num,
  404. 'tool': self.toolslines[num]['tool'],
  405. 'thresh': self.toolslines[num]['thresh']})
  406. def OnSelect(self, event):
  407. """Tool was selected"""
  408. id = event.GetId()
  409. if self.selected > -1 and self.selected != id:
  410. win = self.FindWindowById(self.selected)
  411. win.SetValue(False)
  412. if self.selected != id:
  413. self.selected = id
  414. else:
  415. self.selected = -1
  416. def OnDone(self, event):
  417. """Command done"""
  418. self.SetStatusText('')
  419. def OnCleaningRun(self, event):
  420. """Builds options and runs v.clean
  421. """
  422. self.GetCmdStrings()
  423. err = list()
  424. for p, name in ((self.inmap, _('Name of input vector map')),
  425. (self.outmap, _('Name for output vector map')),
  426. (self.tools_string, _('Tools')),
  427. (self.thresh_string, _('Threshold'))):
  428. if not p:
  429. err.append(_("'%s' not defined") % name)
  430. if err:
  431. GError(_("Some parameters not defined. Operation "
  432. "canceled.\n\n%s") % '\n'.join(err),
  433. parent=self)
  434. return
  435. self.SetStatusText(_("Executing selected cleaning operations..."))
  436. snum = len(self.toolslines.keys())
  437. if self.log:
  438. cmd = [self.cmd,
  439. 'input=%s' % self.inmap,
  440. 'output=%s' % self.outmap,
  441. 'tool=%s' % self.tools_string,
  442. 'thres=%s' % self.thresh_string]
  443. if self.ftype_string:
  444. cmd.append('type=%s' % self.ftype_string)
  445. if self.overwrite.IsChecked():
  446. cmd.append('--overwrite')
  447. self.log.RunCmd(cmd, onDone=self.OnDone)
  448. self.parent.Raise()
  449. else:
  450. if self.overwrite.IsChecked():
  451. overwrite = True
  452. else:
  453. overwrite = False
  454. RunCommand(self.cmd,
  455. input=self.inmap,
  456. output=self.outmap,
  457. type=self.ftype_string,
  458. tool=self.tools_string,
  459. thresh=self.thresh_string,
  460. overwrite=overwrite)
  461. def OnClose(self, event):
  462. self.Destroy()
  463. def OnHelp(self, event):
  464. """Show GRASS manual page"""
  465. RunCommand('g.manual',
  466. quiet=True,
  467. parent=self,
  468. entry=self.cmd)
  469. def OnCopy(self, event):
  470. """Copy the command"""
  471. cmddata = wx.TextDataObject()
  472. # get tool and thresh strings
  473. self.GetCmdStrings()
  474. cmdstring = '%s' % (self.cmd)
  475. # list -> string
  476. cmdstring += ' input=%s output=%s type=%s tool=%s thres=%s' % (
  477. self.inmap, self.outmap, self.ftype_string, self.tools_string, self.thresh_string)
  478. if self.overwrite.IsChecked():
  479. cmdstring += ' --overwrite'
  480. cmddata.SetText(cmdstring)
  481. if wx.TheClipboard.Open():
  482. wx.TheClipboard.SetData(cmddata)
  483. wx.TheClipboard.Close()
  484. self.SetStatusText(
  485. _("Vector cleaning command copied to clipboard"))
  486. def GetCmdStrings(self):
  487. self.tools_string = ''
  488. self.thresh_string = ''
  489. self.ftype_string = ''
  490. # feature types
  491. first = 1
  492. for num in range(0, self.n_ftypes - 1):
  493. if self.ftype_check[num].IsChecked():
  494. if first:
  495. self.ftype_string = '%s' % self.ftype[num]
  496. first = 0
  497. else:
  498. self.ftype_string += ',%s' % self.ftype[num]
  499. # cleaning tools
  500. first = 1
  501. snum = len(self.toolslines.keys())
  502. for num in range(1, snum + 1):
  503. if self.toolslines[num]['tool']:
  504. if first:
  505. self.tools_string = '%s' % self.toolslines[num]['tool']
  506. self.thresh_string = '%s' % self.toolslines[num]['thresh']
  507. first = 0
  508. else:
  509. self.tools_string += ',%s' % self.toolslines[num]['tool']
  510. self.thresh_string += ',%s' % self.toolslines[
  511. num]['thresh']
  512. self.inmap = self.selectionInput.GetValue()
  513. self.outmap = self.selectionOutput.GetValue()
  514. if __name__ == '__main__':
  515. app = wx.App()
  516. frame = VectorCleaningFrame(parent=None)
  517. frame.Show()
  518. app.MainLoop()