39
39
from PyQt4.QtCore import *
40
40
from PyQt4.QtGui import *
41
41
from PyQt4.Qwt5 import QwtPlot, QwtPlotCurve, QwtPlotGrid, QwtLegend
42
import PyQt4.Qwt5.anynumpy as np
43
44
from Ui_main import Ui_MainWindow
44
import reactifs, acidebase
45
import reactifs, acidebase, version
46
import sys, pickle, os.path, re
47
48
class acidoBasicMainWindow(QMainWindow):
48
def __init__(self, parent):
49
def __init__(self, parent, argv):
52
@param parent une fenêtre parente
53
@argv une liste d'arguments
50
56
QMainWindow.__init__(self)
51
57
self.windowTitle="pyAcidoBasic"
52
58
QWidget.__init__(self, parent)
53
59
self.ui = Ui_MainWindow()
61
self.courbesActives=[] # la liste des courbes qu'on peut montrer/cacher
54
62
self.ui.setupUi(self)
55
# on se prépare à changer le traceur de courbe fourni par
56
# l'interface de développement rapide, pour en prendre un
57
# autre qui en dérive et qui a des propriétés définies dans le
59
php=phPlot.phPlot(self.ui.frameCourbes)
60
php.setTitle(u"Courbe du dosage")
61
php.setAxisTitle(QwtPlot.xBottom,"V (mL)")
62
php.setAxisTitle(QwtPlot.yLeft,"pH")
63
self.ui.horizontalLayout_3.removeWidget(self.ui.qwtPlotpH)
63
# substitution des traceurs de courbes
64
####### traceur orienté pH
65
self.ui.horizontalLayout_labCourbes.removeWidget(self.ui.qwtPlotpH)
64
66
self.ui.qwtPlotpH.close()
67
php=phplot.phPlot(self.ui.tabWidget)
65
68
self.ui.qwtPlotpH=php
66
self.ui.horizontalLayout_3.insertWidget (0,php)
67
self.maxBurette=20 # volume max versé de la burette
69
self.ui.horizontalLayout_labCourbes.insertWidget (0,php)
70
####### traceur orienté concentrations
71
self.ui.horizontalLayout_concentrationCourbes.removeWidget(self.ui.qwtConcentrationPlot)
72
self.ui.qwtConcentrationPlot.close()
73
php=phplot.phPlot(self.ui.tabWidget)
74
self.ui.qwtConcentrationPlot=php
75
self.ui.horizontalLayout_concentrationCourbes.insertWidget (1,php)
77
self.plots=(self.ui.qwtPlotpH,self.ui.qwtConcentrationPlot)
78
# volume max versé de la burette
68
80
self.configurePlot()
69
81
reactifs.setupUi(self.ui,acidebase.listFromFile("/usr/share/pyacidobasic/acides-bases.html"))
70
82
self.ui_connections()
84
if os.path.exists(argv[1]):
72
87
def ui_connections(self):
73
88
QObject.connect(self.ui.toolButtonBurette,SIGNAL("clicked()"), self.videBurette)
74
89
QObject.connect(self.ui.toolButtonBecher,SIGNAL("clicked()"), self.videBecher)
75
90
QObject.connect(self.ui.toolButtonPlusV,SIGNAL("clicked()"), self.plusV)
76
91
QObject.connect(self.ui.toolButtonMoinsV,SIGNAL("clicked()"), self.moinsV)
77
QObject.connect(self.ui.PDFbutton,SIGNAL('clicked()'),self.ui.qwtPlotpH.exportPDF)
78
QObject.connect(self.ui.SVGbutton,SIGNAL('clicked()'),self.ui.qwtPlotpH.exportSVG)
79
QObject.connect(self.ui.JPGbutton,SIGNAL('clicked()'),self.ui.qwtPlotpH.exportJPG)
80
QObject.connect(self.ui.TitleButton,SIGNAL('clicked()'),self.ui.qwtPlotpH.newTitle)
92
QObject.connect(self.ui.toolButtonPlusV_2,SIGNAL("clicked()"), self.plusV)
93
QObject.connect(self.ui.toolButtonMoinsV_2,SIGNAL("clicked()"), self.moinsV)
94
QObject.connect(self.ui.PDFbutton,SIGNAL('clicked()'),self.plots[0].exportPDF)
95
QObject.connect(self.ui.SVGbutton,SIGNAL('clicked()'),self.plots[0].exportSVG)
96
QObject.connect(self.ui.JPGbutton,SIGNAL('clicked()'),self.plots[0].exportJPG)
97
QObject.connect(self.ui.TitleButton,SIGNAL('clicked()'),self.plots[0].newTitle)
98
QObject.connect(self.ui.PDFbutton_2,SIGNAL('clicked()'),self.plots[1].exportPDF)
99
QObject.connect(self.ui.SVGbutton_2,SIGNAL('clicked()'),self.plots[1].exportSVG)
100
QObject.connect(self.ui.JPGbutton_2,SIGNAL('clicked()'),self.plots[1].exportJPG)
101
QObject.connect(self.ui.TitleButton_2,SIGNAL('clicked()'),self.plots[1].newTitle)
102
QObject.connect(self.ui.actionQuitter,SIGNAL('triggered()'),self.close)
103
QObject.connect(self.ui.actionEnregistrer_Sous,SIGNAL('triggered()'),self.saveAs)
104
QObject.connect(self.ui.actionEnregistrer,SIGNAL('triggered()'),self.save)
105
QObject.connect(self.ui.actionOuvrir,SIGNAL('triggered()'),self.load)
106
QObject.connect(self.ui.tableWidgetConcentrations,SIGNAL('itemChanged(QTableWidgetItem*)'),self.concentrationChanged)
107
QObject.connect(self.ui.tableWidgetConcentrations,SIGNAL('cellDoubleClicked(int,int)'),self.colorChanged)
110
def concentrationChanged(self, item):
112
fonction de rappel quand on a cliqué sur un item de concentration
113
@param item une instance de QTableWidgetItem
115
self.toggleCourbes() ### vérifie le statut d'affichage et réaffiche
117
def colorChanged(self, row, col):
119
réponse au clic et au double clic
120
@param row la ligne cliquée
121
@param col la colonne cliquée
124
print "GRRR couleur à changer"
125
iCoul=self.ui.tableWidgetConcentrations.item(row,col)
126
color=iCoul.background().color()
127
qd=QColorDialog(color)
129
color=qd.selectedColor()
130
iCoul.setBackground(color)
131
i=self.ui.tableWidgetConcentrations.item(row,0)
133
for c in self.courbesActives:
138
print "GRRRR nouvelle couleur", qd.selectedColor()
147
def nouvelleCourbe(self, titre="", yAxis=QwtPlot.yRight, color="black",
148
width=1, yData=[], cell=None,
149
qwtp=None, enabled=True, checked=False):
151
crée une nouvelle courbe active qu'on peut cacher/montrer
152
@param titre label pour la courbe
153
@param yaxis ordonnées pour la courbe, QwtPlot.yRight par défaut
154
@param color la couleur, "black" par défaut
155
@param width l'épaisseur, 1 par défaut
156
@param yData la liste de données pour Y (c'est self.vData pour X)
157
@param qwtp un qwtPlotWidget, celui des Ĥ par défaut
158
@param enabled si on doit mettre une case active dans le tableau (vrai par défaut)
159
@param checked si la case est pré-cochée (faux par défaut)
160
param cell une cellule de tableau (None par défaut, alors la cellule est créée)
161
@return la cellule de tableau éventuellement créée
163
if self.row > 3 and color=="black":
165
color=self.varColor()
166
c=QwtPlotCurve(titre)
168
i=QTableWidgetItem(titre)
170
i.setFlags(Qt.ItemIsUserCheckable|Qt.ItemIsEnabled)
172
i.setFlags(Qt.ItemIsUserCheckable)
177
t=self.ui.tableWidgetConcentrations
178
t.setRowCount(self.row+1)
179
t.setItem(self.row,0,i)
180
c.cell=t.item(self.row,0)
181
i=QTableWidgetItem("--")
182
i.setFlags(Qt.ItemIsEnabled)
183
i.setData(Qt.BackgroundRole, QColor(color))
184
t.setItem(self.row,1,i)
185
self.courbesActives.append(c)
189
self.courbesActives.append(c)
190
self.legend.insert(c, QLabel(titre))
192
p=QPen(QColor(color))
195
c.setData(self.vData, yData)
197
qwtp=self.ui.qwtPlotpH
203
@return une nouvelle couleur pour chaque self.row
205
r=np.sin(self.row*np.pi/2)
206
v=np.sin(self.row*np.pi/3)
207
b=np.sin(self.row*np.pi/5)
213
def toggleCourbes(self):
215
Affiche ou cache les courbes selon le contexte des cases cochées
217
for c in self.courbesActives:
219
if item.checkState()>0:
220
c.setStyle(QwtPlotCurve.Lines)
222
c.setStyle(QwtPlotCurve.NoCurve)
227
Enregistre les données du graphique sous un nouveau fichier
229
fname=QFileDialog.getSaveFileName (None, u"Fichier pour enregistrer", filter="Fichiers Acidobasic [*.acb] (*.acb);; Tous types de fichiers (*.* *)" )
231
if not re.match("\.\s*$",fname):
232
fname+=".acb" #on ajoute un suffixe par défaut
238
Enregistre les données du graphique
241
p=pickle.Pickler(open(self.filename,"w"))
242
p.dump(version.versionString)
243
burette=self.ui.listWidgetBurette.dump()
245
p.dump(self.ui.listWidgetBecher.dump())
249
def load(self, fname=""):
251
Récupère les données depuis un fichier
254
fname=QFileDialog.getOpenFileName (None, u"Fichier pour enregistrer", filter="Fichiers Acidobasic [*.acb] (*.acb);; Tous types de fichiers (*.* *)" )
257
p=pickle.Unpickler(open(self.filename,"r"))
259
versionString=p.load()
260
if versionString!=version.versionString:
261
raise EnvironmentError
262
self.ui.listWidgetBurette.load(p.load())
263
self.ui.listWidgetBecher.load(p.load())
265
QMessageBox.warning (None, u"Erreur de version", u"Ce fichier n'est pas un fichier Pymecavideo valide, de version %s." %version.version)
82
268
def configurePlot(self):
83
self.ui.qwtPlotpH.setAxisScale(QwtPlot.yLeft,0,14)
84
self.grid=QwtPlotGrid()
269
self.setGrid(self.ui.qwtPlotpH,
272
self.setGrid(self.ui.qwtConcentrationPlot,
274
yLeftTitre="Concentration (mol/L)",
279
def setGrid(self, qwtp, titre=u"Courbe du dosage",
284
yRightTitre="Concentration (mol/L)",
287
définit la grille et les axes
288
@param qwtp un qwtPlotWidget
289
@param le titre initial
290
@param yLeftRange l'intervalle des valeurs sur l'axe Y gauche, (0,14) par défaut
291
@param yRightRange l'intervalle des valeurs sur l'axe Y droit, None par défaut
292
@param xTitre le label de l'axe X, "V (mL)" par défaut
293
@param yLeftTitre le label de l'axe Y de gauche, "pH" par défaut
294
@param yRightTitre le label de l'axe Y de droite, "Concentration (mol/L)" par défaut
296
qwtp.setAxisScale(QwtPlot.yLeft,yLeftRange[0],yLeftRange[1])
297
qwtp.setAxisTitle(QwtPlot.xBottom,xTitre)
298
qwtp.setAxisTitle(QwtPlot.yLeft,yLeftTitre)
299
if yRightRange!=None:
300
qwtp.enableAxis(QwtPlot.yRight)
301
qwtp.setAxisScale(QwtPlot.yRight,yRightRange[0],yRightRange[1])
302
qwtp.setAxisTitle(QwtPlot.yRight,yRightTitre)
85
304
p1=QPen(QColor(100,100,100,100)) # noir transparent
87
306
p2=QPen(QColor(200,200,200,100)) # noir transparent
89
self.grid.setMajPen(p1)
90
self.grid.setMinPen(p2)
91
self.grid.enableX(True)
92
self.grid.enableY(True)
93
self.grid.enableXMin(True)
94
self.grid.enableYMin(True)
95
self.grid.attach(self.ui.qwtPlotpH)
312
grid.enableXMin(True)
313
grid.enableYMin(True)
316
qwtp.setAxisScale(QwtPlot.xBottom,0,self.maxBurette)
99
self.ui.qwtPlotpH.setAxisScale(QwtPlot.xBottom,0,self.maxBurette)
100
self.ui.qwtPlotpH.setAxisTitle(QwtPlot.xBottom,"V (mL)")
101
self.ui.qwtPlotpH.setAxisTitle(QwtPlot.yLeft,"pH")
102
self.ui.qwtPlotpH.setTitle(u"Courbe du dosage")
319
for qwtp in self.plots:
320
qwtp.setAxisScale(QwtPlot.xBottom,0,self.maxBurette)
103
321
self.legend=QwtLegend()
104
322
self.ui.qwtPlotpH.insertLegend(self.legend)
105
self.ui.qwtPlotpH.replot()
323
for qwtp in self.plots:
107
326
def videBurette(self):
108
327
self.ui.listWidgetBurette.vide()
115
330
def videBecher(self):
116
331
self.ui.listWidgetBecher.vide()
124
335
self.maxBurette+=5
132
343
def simule(self):
134
345
self.courbe.detach()
346
for c in self.courbesActives:
137
self.courbe=QwtPlotCurve("pH")
138
p1=QPen(QColor("red"))
140
self.courbe.setPen(p1)
350
if not self.simulationPossible():
354
self.vData=[]; self.pHData=[]; self.derivData=[]
143
356
for i in range (1401):
144
357
pH=0.01*i # pH varie de 0 à 14
146
359
v=-self.ui.listWidgetBecher.charge(pH)/self.ui.listWidgetBurette.charge(pH,1)
148
vData.append(v); pHData.append(pH)
362
self.pHData.append(pH)
363
if len(self.pHData)>1:
364
self.derivData.append(0.01/(self.vData[-1]-self.vData[-2]))
366
self.derivData.append(0.0)
149
367
if pH > self.maxpH: self.maxpH=pH
150
368
except ZeroDivisionError:
152
if len(self.ui.listWidgetBecher.contenu)>0 and len(self.ui.listWidgetBurette.contenu)>0:
153
self.courbe.setData(vData, pHData)
154
self.legend.insert(self.courbe, QLabel("pH"))
155
self.courbe.attach(self.ui.qwtPlotpH)
370
self.courbesConcentrations()
371
self.ajusteMenuGraphiques()
156
373
# essaie d'ajuster le volume de burette pour voir la partie intéressante
158
375
# trouve l'abscisse pour avoir ph < maxpH - 0.5
160
while pHData[i] < self.maxpH - 0.5:
377
while self.pHData[i] < self.maxpH - 0.5:
163
380
# ajuste le volume versé depuis la burette à 5mL près
164
381
self.maxBurette=5*(1+int(v)/5)
384
self.toggleCourbes() ### vérifie le statut d'affichage et réaffiche
386
def ajusteMenuGraphiques(self):
388
Ajoute toutes les espèces présentes, dans le menu du graphique
390
burette=self.ui.listWidgetBurette
391
becher=self.ui.listWidgetBecher
393
for c in becher.contenu:
395
vTotal=totalBecher*np.ones(len(self.pHData))+self.vData
398
for i in range(len(c.formes)):
399
self.nouvelleCourbe(titre=c.formes[i],
402
yData=c.concentrationParPH(i,self.pHData,vTotal, vBurette=self.vData),
403
qwtp=self.ui.qwtConcentrationPlot,
407
for c in becher.contenu:
408
for i in range(len(c.formes)):
409
self.nouvelleCourbe(titre=c.formes[i],
412
yData=c.concentrationParPH(i,self.pHData,vTotal),
413
qwtp=self.ui.qwtConcentrationPlot,
417
def courbesConcentrations(self):
419
Crée les courbes des concentrations
421
t=self.ui.tableWidgetConcentrations
423
t.setHorizontalHeaderLabels([u"Espèce",u"Cl"])
424
t.setVerticalHeaderLabels(["","","","",""])
425
t.setColumnWidth(0,170)
426
t.setColumnWidth(1,20)
428
cell=self.nouvelleCourbe(titre="pH",
433
qwtp=self.ui.qwtPlotpH,
438
cell=self.nouvelleCourbe(titre="pH",
439
color=QColor(255,0,0,50),
440
yAxis=QwtPlot.yRight,
443
qwtp=self.ui.qwtConcentrationPlot,
446
d=np.array(self.derivData)
448
d= 14.0+d # décalage en cas de dosage par un acide !
452
cell=self.nouvelleCourbe(titre=titre,
457
qwtp=self.ui.qwtPlotpH,
459
cell=self.nouvelleCourbe(titre=titre,
460
color=QColor(0,0,255,50),
461
yAxis=QwtPlot.yRight,
464
qwtp=self.ui.qwtConcentrationPlot,
466
H3OData=np.exp(- 2.302585093*np.array(self.pHData))
467
cell=self.nouvelleCourbe(titre="[H3O+]",
471
qwtp=self.ui.qwtPlotpH,
473
cell=self.nouvelleCourbe(titre="[H3O+]",
478
qwtp=self.ui.qwtConcentrationPlot,
481
cell=self.nouvelleCourbe(titre="[HO-]",
485
qwtp=self.ui.qwtPlotpH,
487
cell=self.nouvelleCourbe(titre="[HO-]",
492
qwtp=self.ui.qwtConcentrationPlot,
496
def simulationPossible(self):
498
@return un boolean vrai si une simulation est possible
500
return len(self.ui.listWidgetBecher.contenu)>0 and len(self.ui.listWidgetBurette.contenu)>0
171
502
def event(self, ev):
172
503
if ev.type()==QEvent.User: