/
mna.py
704 lines (627 loc) · 34.8 KB
/
mna.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Mna - A Currency Converter program
# Copyright (c) 2012-2021, Petros Kyladitis <http://www.multipetros.gr/>
# This is free software, distributed under the FreeBSD Lisence
import wx
import wx.lib.plot as plot
import webbrowser
import ConfigParser
import xml.etree.ElementTree as ET
import threading
from datetime import datetime, timedelta
from wx.lib.wordwrap import wordwrap
from urllib2 import urlopen
from sys import platform
from os.path import expanduser, getmtime, exists
from os import getenv
################################################################################
class MainFrame(wx.Frame):
# All ECB supported currencies
currencies=["Australian Dollar (AUD)",
"Brazilian Real (BRL)",
"Bulgarian Lev (BGN)",
"Canadian Dollar (CAD)",
"Chinese Yuan (CNY)",
"Croatian Kuna (HRK)",
"Czech Koruna (CZK)",
"Danish Krone (DKK)",
"Euro (EUR)",
"Great British Pound (GBP)",
"Hong Kong Dollar (HKD)",
"Hungarian Forint (HUF)",
"Icelandic Krona (ISK)",
"Indian Rupee (INR)",
"Indonesian Rupiah (IDR)",
"Israeli Shekel (ILS)",
"Japanese Yen (JPY)",
"Malaysian Ringgit (MYR)",
"Mexican Peso (MXN)",
"New Zealand Dollar (NZD)",
"Norwegian Krone (NOK)",
"Philippine Peso (PHP)",
"Polish Zloty (PLN)",
"Romanian Leu (RON)",
"Russian Rouble (RUB)",
"Singapore Dollar (SGD)",
"South African Rand (ZAR)",
"South Korean Won (KRW)",
"Swedish Krona (SEK)",
"Swiss Franc (CHF)",
"Thai Baht (THB)",
"Turkish Lira (TRY)",
"US Dollar (USD)"]
# General constants
PRODUCT = "Mna"
VERSION = "1.7.0"
# Constants Ini & XML path, section, parameters
if platform == "win32":
INI_FILE = getenv('APPDATA') + "\\mna.cfg"
XML_FILE = getenv('APPDATA') + "\\mna.xml"
else:
INI_FILE = expanduser("~/.mna.cfg")
XML_FILE = expanduser("~/.mna.xml")
INI_SECTION = "main"
INI_PARAM_FROM = "from"
INI_PARAM_TO = "to"
INI_PARAM_PRECISION = "precision"
def __init__(self, *args, **kwds):
# Initialize variables for curencies rates
self.rates_dic = {} # empty dictionary
self.rates_day = "00" # zero day, used to retrieve new data XML parsing error
# Initialize variables that determinate the need of retrieve fresh data from the network
self.current_currency = 0 # the current curenncies exchange rate
self.last_from = "" # the last selected 'from' currency
self.last_to = "" # the last selected 'to' currency
kwds["style"] = wx.CAPTION | wx.CLOSE_BOX | wx.MINIMIZE_BOX | wx.SYSTEM_MENU | wx.TAB_TRAVERSAL | wx.CLIP_CHILDREN
wx.Frame.__init__(self, *args, **kwds)
# Main frame controls
self.label_from = wx.StaticText(self, -1, "From")
self.combo_box_from = wx.ComboBox(self, -1, choices=self.currencies, style=wx.CB_DROPDOWN | wx.CB_DROPDOWN | wx.CB_READONLY)
self.combo_box_from.SetToolTip(wx.ToolTip("Select the currency you want to convert from"))
self.text_ctrl_from = wx.TextCtrl(self, -1, "")
self.text_ctrl_from.SetToolTip(wx.ToolTip("amount you want to convert"))
self.label_to = wx.StaticText(self, -1, "To")
self.combo_box_to = wx.ComboBox(self, -1, choices=self.currencies, style=wx.CB_DROPDOWN | wx.CB_DROPDOWN | wx.CB_READONLY)
self.combo_box_to.SetToolTip(wx.ToolTip("Select the currency you want to convert to"))
self.text_ctrl_to = wx.TextCtrl(self, -1, "", style=wx.TE_READONLY)
self.text_ctrl_to.SetToolTip(wx.ToolTip("Converted amount"))
self.button_convert = wx.Button(self, -1, "Convert")
self.button_convert.SetToolTip(wx.ToolTip("Click to convert the amount to selected currency"))
self.button_convert.SetDefault()
# Menu Bar
self.frame_main_menubar = wx.MenuBar()
self.menu_file = wx.Menu()
self.menu_item_plot= wx.MenuItem(self.menu_file, wx.NewId(), "&Plot rates\tCtrl+P", "Plot rates of last 90 days ", wx.ITEM_NORMAL)
self.menu_file.AppendItem(self.menu_item_plot)
self.menu_item_retrieve= wx.MenuItem(self.menu_file, wx.NewId(), "&Retrieve rates\tCtrl+R", "Force program to retrieve rates", wx.ITEM_NORMAL)
self.menu_file.AppendItem(self.menu_item_retrieve)
self.menu_item_ratesinfo = wx.MenuItem(self.menu_file, wx.NewId(), "Rates in&fo\tCtrl+I", "Show info about the rates", wx.ITEM_NORMAL)
self.menu_file.AppendItem(self.menu_item_ratesinfo)
# If NOT run on OSX create and append an Exit menu item to the File menu. At OSX is unnecessary because of App Menu
if platform != "darwin":
self.menu_file.AppendSeparator()
self.menu_item_exit = wx.MenuItem(self.menu_file, wx.ID_EXIT, "&Exit\tCtrl+Q", "Quit the program", wx.ITEM_NORMAL)
self.menu_file.AppendItem(self.menu_item_exit)
self.frame_main_menubar.Append(self.menu_file, "&File")
self.menu_precision = wx.Menu()
self.menu_item_two_decs = wx.MenuItem(self.menu_precision, 202, "&2 decimals\tCtrl+2", "Precision with 2 decimal digits", wx.ITEM_RADIO)
self.menu_precision.AppendItem(self.menu_item_two_decs)
self.menu_item_four_decs = wx.MenuItem(self.menu_precision, 204, "&4 decimals\tCtrl+4", "Precision with 4 decimal digits", wx.ITEM_RADIO)
self.menu_precision.AppendItem(self.menu_item_four_decs)
self.menu_item_six_decs = wx.MenuItem(self.menu_precision, 206, "&6 decimals\tCtrl+6", "Precision with 6 decimal digits", wx.ITEM_RADIO)
self.menu_precision.AppendItem(self.menu_item_six_decs)
self.menu_item_eight_decs = wx.MenuItem(self.menu_precision, 208, "&8 decimals\tCtrl+8", "Precision with 8 decimal digits", wx.ITEM_RADIO)
self.menu_precision.AppendItem(self.menu_item_eight_decs)
self.frame_main_menubar.Append(self.menu_precision, "&Precision")
self.menu_help = wx.Menu()
self.menu_item_updates = wx.MenuItem(self.menu_help, wx.NewId(), "&Check for updates\tCtrl+U", "Go to website to check if newer versions exist", wx.ITEM_NORMAL)
self.menu_help.AppendItem(self.menu_item_updates)
self.menu_item_about = wx.MenuItem(self.menu_help, wx.ID_ABOUT, "&About\tF1", "Show about info", wx.ITEM_NORMAL)
self.menu_help.AppendItem(self.menu_item_about)
self.frame_main_menubar.Append(self.menu_help, "&Help")
self.SetMenuBar(self.frame_main_menubar)
# Add status bar
self.statusbar = self.CreateStatusBar()
self.__set_properties()
self.__do_layout()
# Add event handlers
self.Bind(wx.EVT_BUTTON, self.OnConvert, self.button_convert)
self.Bind(wx.EVT_COMBOBOX, self.OnConvert, self.combo_box_from)
self.Bind(wx.EVT_COMBOBOX, self.OnConvert, self.combo_box_to)
# If NOT run on OSX bind handler for Exit menu item
if platform != "darwin":
self.Bind(wx.EVT_MENU, self.OnExit, self.menu_item_exit)
self.Bind(wx.EVT_CLOSE, self.OnExit)
self.Bind(wx.EVT_MENU, self.OnPlot, self.menu_item_plot)
self.Bind(wx.EVT_MENU, self.OnRetrieve, self.menu_item_retrieve)
self.Bind(wx.EVT_MENU, self.OnRatesInfo, self.menu_item_ratesinfo)
self.Bind(wx.EVT_MENU, self.OnPrecisionChange, self.menu_item_two_decs)
self.Bind(wx.EVT_MENU, self.OnPrecisionChange, self.menu_item_four_decs)
self.Bind(wx.EVT_MENU, self.OnPrecisionChange, self.menu_item_six_decs)
self.Bind(wx.EVT_MENU, self.OnPrecisionChange, self.menu_item_eight_decs)
self.Bind(wx.EVT_MENU, self.OnUpdates, self.menu_item_updates)
self.Bind(wx.EVT_MENU, self.OnAbout, self.menu_item_about)
# Bind event handler for double click on status bar
self.statusbar.Bind(wx.EVT_LEFT_DCLICK, self.OnDblClickStatus, self.statusbar)
# load rates, using another thread
self.LoadStartupRates()
#---------------------------------------------------------------------------
def __set_properties(self):
self.SetTitle("Mna Currency Converter")
self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))
# Application icon
self.ico = wx.Icon("icon.gif", wx.BITMAP_TYPE_GIF)
self.SetIcon(self.ico)
# Load ini parameters and set the selected values at decimals and rates combo boxes
# If ini or parameters missing, not valid or other error set selected 2 decimals, EUR and USD as rates
try:
config = ConfigParser.ConfigParser()
config.read(self.INI_FILE)
self.current_precision = int(config.get(self.INI_SECTION, self.INI_PARAM_PRECISION))
self.combo_box_from.SetSelection(long(config.get(self.INI_SECTION, self.INI_PARAM_FROM)))
self.combo_box_to.SetSelection(long(config.get(self.INI_SECTION, self.INI_PARAM_TO)))
except:
self.current_precision = 2
self.combo_box_from.SetSelection(8)
self.combo_box_to.SetSelection(32)
# Start-up values, used to determinate selection changes at the program's exit
self.init_from = self.combo_box_from.GetCurrentSelection()
self.init_to = self.combo_box_to.GetCurrentSelection()
self.init_precision = self.current_precision
self.SetInitPrecision(self.init_precision)
#---------------------------------------------------------------------------
def __do_layout(self):
# Use a vertical box sizer to fit menu bar, grid sizer with controls & status bar
box_sizer = wx.BoxSizer(wx.VERTICAL)
# Place controls to a flex grid sizer, with empty cells arround them
grid_sizer_main = wx.FlexGridSizer(5, 5, 6, 6)
for x in range(6):
grid_sizer_main.Add((5, 5), 0, 0, 0)
grid_sizer_main.Add(self.label_from, 0, 0, 0)
grid_sizer_main.Add(self.combo_box_from, 0, 0, 0)
grid_sizer_main.Add(self.text_ctrl_from, 0, 0, 0)
grid_sizer_main.Add((5, 5), 0, 0, 0)
grid_sizer_main.Add((5, 5), 0, 0, 0)
grid_sizer_main.Add(self.label_to, 0, 0, 0)
grid_sizer_main.Add(self.combo_box_to, 0, 0, 0)
grid_sizer_main.Add(self.text_ctrl_to, 0, 0, 0)
for x in range(4):
grid_sizer_main.Add((5, 5), 0, 0, 0)
grid_sizer_main.Add(self.button_convert, 0, wx.ALIGN_CENTER_HORIZONTAL, 0)
for x in range(6):
grid_sizer_main.Add((5, 5), 0, 0, 0)
# Add grid sizer with controls to box sizer and set the box sizer
# as the sizer for the main frame
box_sizer.Add(grid_sizer_main, 1, wx.EXPAND, 0)
self.SetSizerAndFit(box_sizer)
box_sizer.Fit(self)
self.Layout()
#---------------------------------------------------------------------------
def LoadStartupRates(self):
# Load rates from file. On error resume next
try:
self.LoadRates()
except:
pass
today = datetime.utcnow()
if exists(self.XML_FILE):
xmldate = datetime.utcfromtimestamp(getmtime(self.XML_FILE))
else:
xmldate = today - timedelta(days=2)
# Check if rates data are not retreived today
# The rates are updated around 16:00 CET, so if yesterday data and time is <16 don't get fresh
if (today.day != xmldate.day and (today - timedelta(hours=(today.hour + 8), minutes=today.minute) > xmldate)) or (today.day == xmldate.day and today.hour >= 16 and xmldate.hour < 16):
#use other thread to retreive data
threading.Thread(target=self.GetFreshData).start()
else:
self.SetStatusText("Latest retrieved rates of " + self.rates_day)
#---------------------------------------------------------------------------
def SetInitPrecision(self, precision):
# Set checked the appropriate menu item, based on precision param
if precision == 4:
self.menu_item_four_decs.Check(True)
elif precision == 6:
self.menu_item_six_decs.Check(True)
elif precision == 8:
self.menu_item_eight_decs.Check(True)
else:
self.menu_item_two_decs.Check(True)
#---------------------------------------------------------------------------
def OnDblClickStatus(self, event):
# If status bar have text, display it on a text box
if self.statusbar.StatusText != "":
wx.MessageBox(self.statusbar.StatusText, "Status info", wx.OK | wx.ICON_INFORMATION)
#---------------------------------------------------------------------------
def OnExit(self, event):
# 1st of all hide the main form
self.Hide()
# If selected currencies differs from the initial selected
# try to save to the ini configuration file, on error just exit
try:
if (long(self.init_from) != self.combo_box_from.GetCurrentSelection()) or (long(self.init_to) != self.combo_box_to.GetCurrentSelection() or self.init_precision != self.current_precision):
cfgfile = open(self.INI_FILE,"w")
config = ConfigParser.ConfigParser()
config.read(self.INI_FILE)
# Try to add the main section, if exist an exception will be thrown, in that case resume next
try:
config.add_section(self.INI_SECTION)
except:
pass
# Set the values to ini parameters and save
config.set(self.INI_SECTION, self.INI_PARAM_PRECISION, str(self.current_precision))
config.set(self.INI_SECTION, self.INI_PARAM_FROM, str(self.combo_box_from.GetCurrentSelection()))
config.set(self.INI_SECTION, self.INI_PARAM_TO, str(self.combo_box_to.GetCurrentSelection()))
config.write(cfgfile)
cfgfile.close()
except:
pass
# destroy plot frame & colse it if open
frame_plot.Destroy()
# destroy main frame to exit app
self.Destroy()
#---------------------------------------------------------------------------
def OnUpdates(self, event):
# open browser to mna repo website
webbrowser.open_new("https://github.com/multipetros/mna/#whats-new")
#---------------------------------------------------------------------------
def OnRatesInfo(self, event):
# show info about the latest retreive date of the rates
if self.rates_day == '00' :
last_rates = 'No rates retrieved'
else :
last_rates = 'Latest rates of ' + self.rates_day
wx.MessageBox(last_rates, 'Rates info', wx.OK | wx.ICON_INFORMATION)
#---------------------------------------------------------------------------
def OnPlot(self, event):
# show plot frame, passing selected currencies
frame_plot.ShowSelected(self.combo_box_from.GetSelection(), self.combo_box_to.GetSelection())
#---------------------------------------------------------------------------
def OnAbout(self, event):
# create and show an about dialog box
info = wx.AboutDialogInfo()
info.SetName("Mna Currency Converter")
info.SetVersion(self.VERSION)
info.SetCopyright("Copyright (C) 2012-2021, Petros Kyladitis")
info.Description = wordwrap("A currency converter program for Python, using wxPython for the GUI and urllib2 library with ECB web service to retrieve updated exchange data.", 350, wx.ClientDC(self))
info.SetWebSite("http://www.multipetros.gr")
info.License = wordwrap("This program is free software, distributed under the terms and conditions of the FreeBSD License. For full licensing info see the \"license.txt\" file, distributed with this program", 350, wx.ClientDC(self))
info.SetIcon(self.ico) # Declared at self.__set_properties()
wx.AboutBox(info)
#---------------------------------------------------------------------------
def OnPrecisionChange(self, event):
# Get the id of the menu item that raise the event. The ids are especially setted
# as (item meaning + 200), so it's easy to find the selected precision value
self.current_precision = event.GetId() - 200
self.DoConversion(False)
#---------------------------------------------------------------------------
def OnConvert(self, event):
# If the current selected currencies is the same as the last conversion,
# start the conversion without retrieve fresh data, else convert with retriving new data
# and save the selected curencies values as the last selected.
if (self.combo_box_from.Value == self.last_from) and (self.combo_box_to.Value == self.last_to):
self.DoConversion(False)
else:
self.DoConversion()
self.last_from = self.combo_box_from.Value
self.last_to = self.combo_box_to.Value
#---------------------------------------------------------------------------
def OnRetrieve(self, event):
# force to load fresh rates data in another thread
threading.Thread(target=self.GetFreshData).start()
#---------------------------------------------------------------------------
def GetFreshData(self):
self.button_convert.Disable()
self.menu_item_retrieve.Enable(False)
self.menu_item_plot.Enable(False)
# download XML with latest rates from ECB website
try:
self.SetStatusText("Retrieving data. Please wait...")
data = urlopen("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml").read()
f = open(self.XML_FILE, "w")
f.write(data)
f.close()
self.LoadRates()
self.SetStatusText("Done! Retrieved rates of " + self.rates_day)
except Exception as details:
self.SetStatusText("Error on retrieve data from ECB. " + str(details))
self.current_currency = 0 # to force retrieve data at the next DoConversion call
finally:
self.button_convert.Enable()
self.menu_item_retrieve.Enable(True)
self.menu_item_plot.Enable(True)
#---------------------------------------------------------------------------
def LoadRates(self):
# parse XML and load rates to dictionary, also set rates day
root = ET.parse(self.XML_FILE).getroot()
self.rates_day = root[2][0].get('time')
self.rates_dic = {}
for child in root[2][0]:
self.rates_dic[child.get('currency')] = float(child.get('rate'))
self.rates_dic["EUR"] = 1.0
if len(self.rates_dic) != 33:
raise Exception("Rates are not loaded properly.")
#---------------------------------------------------------------------------
def DoConversion(self, new_currencies=True):
# try to convert the user inputed amount, converting comma to period if used for decimal point, into an absolute float, on error set it to 1.0
try:
user_input_amount = abs(float(self.text_ctrl_from.Value.replace(',','.')))
except:
user_input_amount = 1.0
self.text_ctrl_from.Value = str(user_input_amount)
# get curency name from the selected combos
from_cur_name = self.combo_box_from.Value[-4:-1]
to_cur_name = self.combo_box_to.Value[-4:-1]
# calc exchange rate and foreing currency amount
self.rate = self.rates_dic[to_cur_name] / self.rates_dic[from_cur_name]
result = user_input_amount * self.rate
self.text_ctrl_to.Value = str(round(result, self.current_precision))
if new_currencies:
self.SetStatusText("Exchange rate: 1 " + from_cur_name + " = " + str(self.rate) + " " + to_cur_name)
################################################################################
class PlotFrame(wx.Frame):
def __init__(self, *args, **kwds):
kwds["style"] = wx.CAPTION | wx.CLOSE_BOX | wx.MINIMIZE_BOX | wx.MAXIMIZE_BOX | wx.RESIZE_BORDER | wx.SYSTEM_MENU | wx.TAB_TRAVERSAL | wx.CLIP_CHILDREN
wx.Frame.__init__(self, *args, **kwds)
# create the widgets
self.canvas = PlotCanvasRates(self, 0, wx.Size(800,600))
self.toggleGrid = wx.CheckBox(self, label="Show Grid")
self.toggleGrid.Bind(wx.EVT_CHECKBOX, self.OnToggleGrid)
self.toggleGrid.SetToolTip(wx.ToolTip("Display or Hide Grid on the Canvas"))
self.label_from = wx.StaticText(self, -1, "From")
self.combo_box_from = wx.ComboBox(self, -1, choices=MainFrame.currencies, style=wx.CB_DROPDOWN | wx.CB_DROPDOWN | wx.CB_READONLY)
self.label_to = wx.StaticText(self, -1, "To")
self.combo_box_to = wx.ComboBox(self, -1, choices=MainFrame.currencies, style=wx.CB_DROPDOWN | wx.CB_DROPDOWN | wx.CB_READONLY)
self.button_reverse = wx.Button(self, -1, "Reverse")
self.button_reverse.SetToolTip(wx.ToolTip("Click to convert the amount to selected currency"))
self.button_save = wx.Button(self, -1, "Save File")
self.button_print = wx.Button(self, -1, "Print")
# make printing menu
self.menu_print = wx.Menu()
self.menu_item_page = wx.MenuItem(self.menu_print, wx.NewId(), 'Page Setup', '', wx.ITEM_NORMAL)
self.menu_print.AppendItem(self.menu_item_page)
self.menu_item_preview = wx.MenuItem(self.menu_print, wx.NewId(), 'Print Preview', '', wx.ITEM_NORMAL)
self.menu_print.AppendItem(self.menu_item_preview)
self.menu_item_print = wx.MenuItem(self.menu_print, wx.NewId(), 'Send to Printer', '', wx.ITEM_NORMAL)
self.menu_print.AppendItem(self.menu_item_print)
# make saving menu
self.menu_save = wx.Menu()
self.menu_item_saveimg = wx.MenuItem(self.menu_save, wx.NewId(), 'Image', '', wx.ITEM_NORMAL)
self.menu_save.AppendItem(self.menu_item_saveimg)
self.menu_item_savetxt = wx.MenuItem(self.menu_save, wx.NewId(), 'Text File', '', wx.ITEM_NORMAL)
self.menu_save.AppendItem(self.menu_item_savetxt)
self.__set_properties()
self.__do_layout()
self.Bind(wx.EVT_CLOSE, self.OnExit)
self.Bind(wx.EVT_COMBOBOX, self.OnCurChange, self.combo_box_to)
self.Bind(wx.EVT_COMBOBOX, self.OnCurChange, self.combo_box_from)
self.Bind(wx.EVT_BUTTON, self.OnReverse, self.button_reverse)
self.Bind(wx.EVT_BUTTON, self.OnSave, self.button_save)
self.Bind(wx.EVT_BUTTON, self.OnPrint, self.button_print)
self.Bind(wx.EVT_MENU, self.OnSendToPrinter, self.menu_item_print)
self.Bind(wx.EVT_MENU, self.OnPrintPreview, self.menu_item_preview)
self.Bind(wx.EVT_MENU, self.OnPageSetup, self.menu_item_page)
self.Bind(wx.EVT_MENU, self.OnSaveImg, self.menu_item_saveimg)
self.Bind(wx.EVT_MENU, self.OnSaveTxt, self.menu_item_savetxt)
#---------------------------------------------------------------------------
def __do_layout(self):
# create sizers
self.mainSizer = wx.BoxSizer(wx.VERTICAL)
self.bottomSizer = wx.BoxSizer(wx.HORIZONTAL)
# layout the widgets
self.mainSizer.Add(self.canvas, 1, wx.EXPAND)
self.bottomSizer.Add(self.label_from, 0, wx.ALL, 5)
self.bottomSizer.Add(self.combo_box_from, 0, wx.ALL, 5)
self.bottomSizer.Add(self.label_to, 0, wx.ALL, 5)
self.bottomSizer.Add(self.combo_box_to, 0, wx.ALL, 5)
self.bottomSizer.Add(self.button_reverse, 0, wx.ALL, 5)
self.bottomSizer.Add(self.toggleGrid, 0, wx.ALL, 5)
self.bottomSizer.Add(self.button_save, 0, wx.ALL, 5)
self.bottomSizer.Add(self.button_print, 0, wx.ALL, 5)
self.mainSizer.Add(self.bottomSizer)
self.SetSizer(self.mainSizer)
self.Layout()
#---------------------------------------------------------------------------
def __set_properties(self):
# frame props
self.SetTitle("Mna Graph of the latest 90 days")
self.SetBackgroundColour(wx.WHITE)
# Application icon
self.ico = wx.Icon("icon.gif", wx.BITMAP_TYPE_GIF)
self.SetIcon(self.ico)
#---------------------------------------------------------------------------
def OnReverse(self, event):
# reverse selected currencies and plot
from_selected = self.combo_box_from.GetSelection()
self.combo_box_from.Select(self.combo_box_to.GetSelection())
self.combo_box_to.Select(from_selected)
self.canvas.PlotRates(self.combo_box_from.Value[-4:-1], self.combo_box_to.Value[-4:-1])
#---------------------------------------------------------------------------
def OnSave(self, event):
# show saving menu under the save button
pos = self.button_save.GetPositionTuple()
size = self.button_save.GetSizeTuple()
self.PopupMenu(self.menu_save, (pos[0], pos[1]+size[1]))
#---------------------------------------------------------------------------
def OnSaveImg(self, event):
# use another thread for image saving
threading.Thread(target=self.SaveImg).start()
#---------------------------------------------------------------------------
def SaveImg(self):
# show common dialog to save plot as image
fd = wx.FileDialog(self, "Save Plot to Image", wildcard="Windows bitmap (*.bmp)|*.bmp|Portable Network Graphics (*.png)|*.png|Joint Photographic Experts Group (*.jpg)|*.jpg|X bitmap (*.xpm)|*.xpm", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
if fd.ShowModal() == wx.ID_CANCEL:
return
path = fd.GetPath()
self.canvas.SaveFile(path)
#---------------------------------------------------------------------------
def OnSaveTxt(self, event):
# use another thread for text saving
threading.Thread(target=self.SaveTxt).start()
#---------------------------------------------------------------------------
def SaveTxt(self):
# show common dialog and save rates date by date, from latest to prior, to file
fd = wx.FileDialog(self, "Save rates to text file", wildcard="Text File (*.txt)|*.txt|All files (*.*)|*.*", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
if fd.ShowModal() == wx.ID_CANCEL:
return
path = fd.GetPath()
from_cur = self.combo_box_from.Value[-4:-1]
to_cur = self.combo_box_to.Value[-4:-1]
txt = '1 ' + from_cur + ' to ' + to_cur + "\n------------\n"
for item in reversed(self.canvas.time_lst):
txt = txt + item[0].strftime("%Y-%m-%d") + " " + str(item[1][to_cur] / item[1][from_cur]) + "\n"
try:
f = open(path, 'w')
f.write(txt)
f.close()
except Exception as details:
wx.MessageBox(str(details), 'Error saving file', wx.OK | wx.ICON_ERROR)
#---------------------------------------------------------------------------
def OnPrint(self, event):
# show printing menu under the print button
pos = self.button_print.GetPositionTuple()
size = self.button_print.GetSizeTuple()
self.PopupMenu(self.menu_print, (pos[0], pos[1]+size[1]))
#---------------------------------------------------------------------------
def OnSendToPrinter(self, event):
self.canvas.Printout()
#---------------------------------------------------------------------------
def OnPageSetup(self, event):
self.canvas.PageSetup()
#---------------------------------------------------------------------------
def OnPrintPreview(self, event):
self.canvas.PrintPreview()
#---------------------------------------------------------------------------
def OnCurChange(self, event):
self.canvas.PlotRates(self.combo_box_from.Value[-4:-1], self.combo_box_to.Value[-4:-1])
#---------------------------------------------------------------------------
def OnToggleGrid(self, event):
self.canvas.SetEnableGrid(event.IsChecked())
#---------------------------------------------------------------------------
def OnExit(self, event):
# hide, but not destroy this form
self.Hide()
#---------------------------------------------------------------------------
def ShowSelected(self, from_cur=8, to_cur=32):
# show this form, initializing selected currencies
self.combo_box_from.Select(from_cur)
self.combo_box_to.Select(to_cur)
self.canvas.PlotRates(self.combo_box_from.Value[-4:-1], self.combo_box_to.Value[-4:-1])
self.Show()
################################################################################
class PlotCanvasRates(plot.PlotCanvas):
time_dic = {}
time_lst = []
from_cur = 8
to_cur = 32
first_day = None
#---------------------------------------------------------------------------
def __init__(self, parent, id, size):
plot.PlotCanvas.__init__(self, parent, id, style=wx.BORDER_NONE, size=wx.Size(800,600))
self.SetEnableAntiAliasing(True)
self.SetEnablePointLabel(True)
self.SetPointLabelFunc(self.DrawPointLabel)
self.canvas.Bind(wx.EVT_MOTION, self.OnMotion)
try:
LoadRates()
except:
pass
#---------------------------------------------------------------------------
def LoadRates(self):
# parse currnecy rates of each date from xml and add them in a dictionary and
# then append the dictionary to a 2-column list, with current date as 1st column
# at the end reverse the list, so latest date goes at the end, and calculate
# starting (oldest) date
root = ET.parse(MainFrame.XML_FILE).getroot()
t = 0
for time_child in root[2]:
rate_dic = {}
for rate_child in root[2][t]:
rate_dic[rate_child.get('currency')] = float(rate_child.get('rate'))
rate_dic["EUR"] = 1.0
self.time_dic[time_child.get('time')] = rate_dic
self.time_lst.append((datetime.strptime(time_child.get('time'), '%Y-%m-%d'), rate_dic))
t = t + 1
last_time = len(self.time_dic) - 1
self.time_lst.reverse()
self.first_day = self.time_lst[0][0]
#---------------------------------------------------------------------------
def PlotRates(self, from_cur, to_cur):
# plot rates to the canvas, using a red polyline
# if dictionary is empty, parse xml before plotting
if len(self.time_dic) < 1:
try:
self.LoadRates()
except:
return
self.Clear()
self.data = []
self.from_cur = from_cur
self.to_cur = to_cur
for item in self.time_lst:
self.data.append(((item[0]-self.first_day).days, item[1][self.to_cur] / item[1][self.from_cur]))
line = plot.PolyLine(self.data, legend='', colour=wx.RED, width=3)
gc = plot.PlotGraphics([line], '1 ' + from_cur + ' to ' + to_cur, 'Last 90 Days', 'Exchange Rate')
self.Draw(gc)
#---------------------------------------------------------------------------
def _xticks(self, *args):
# override default PlotCanvas behavior and display serial days as date strings
ticks = plot.PlotCanvas._xticks(self, *args)
dateTicks = []
for tick in ticks:
floatVal = tick[0]
stringVal = (self.first_day + timedelta(days=floatVal)).strftime('%b%d')
dateTicks.append((floatVal, stringVal))
return dateTicks
#---------------------------------------------------------------------------
def OnMotion(self, event):
# show closest point
if self.GetEnablePointLabel() == True:
# create dictinonary with info for the pointLabel
# I've decided to mark the closest point on the closest curve
dlst = self.GetClosestPoint( self._getXY(event), pointScaled= True)
if dlst != []:
curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
# make up dictionary to pass to DrawPointLabel function
mDataDict= {"pointXY":pointXY, "scaledXY":scaledXY, "pIndex": pIndex}
# pass dictionary to update the pointLabel
self.UpdatePointLabel(mDataDict)
event.Skip() #go to next handler
#---------------------------------------------------------------------------
def DrawPointLabel(self, dc, mDataDict):
# plot pointLabels, displaying a line with value and a line with date
# dc : DC that will be passed
# mDataDict : Dictionary of motion data
dc.SetPen(wx.Pen(wx.BLACK))
dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ))
sx, sy = mDataDict["scaledXY"] # scaled x,y of closest point
dc.DrawRectangle(sx-5, sy-5, 10, 10) # 10by10 square centered on point
px, py = mDataDict["pointXY"]
curRate = self.time_lst[mDataDict["pIndex"]][1][self.to_cur] / self.time_lst[mDataDict["pIndex"]][1][self.from_cur]
d = 0
while int(curRate * pow(10, d)) == 0: # calc decimals, to deterinate needed round digits
d = d + 1
line1 = str(round(self.time_lst[mDataDict["pIndex"]][1][self.to_cur] / self.time_lst[mDataDict["pIndex"]][1][self.from_cur], d+4))
line2 = self.time_lst[mDataDict["pIndex"]][0].strftime('%b %d')
x1, y1 = dc.GetTextExtent(line1)
x2, y2 = dc.GetTextExtent(line2)
dc.DrawText(line1, sx, sy+11)
dc.DrawText(line2, sx-(x2-x1)/2, sy+y1+13)
################################################################################
class MnaApp(wx.App):
#---------------------------------------------------------------------------
# Override method for handling kAEReopenApplication to work correctly with the Dock at OSX
def MacReopenApp(self):
try:
self.GetTopWindow().Raise()
except:
pass
################################################################################
if __name__ == "__main__":
app = MnaApp(False)
frame_main = MainFrame(None, -1, "")
frame_plot = PlotFrame(None, -1, size=wx.Size(800,600))
app.SetTopWindow(frame_main)
frame_main.Show()
app.MainLoop()