vclean.py 21 KB

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