Ohjelmoinnin peruskurssien laaja oppimäärä Luento 6: Graasten käyttöliittymien ohjelmointia Riku Saikkonen (osa kalvoista on suoraan ei-laajan kurssin luennoista) 29. 2. 2012
Sisältö 1 GUI-ohjelmointia Qt:lla, tapahtumankäsittely 2 Komponenttien asettelu 3 Sekalaista GUI-ohjelmoinnista 4 Omien komponenttien piirtäminen
(ei-laajan kurssin kalvo: luento 6 sivu 6) PyQt PyQt ei ainoa mahdollisuus (PySide, Tkinter, wxpython,... Tkinter on vanha Pythonin mukana tuleva kirjasto PyQt Ei erillisiä asennuksia Joidenkin mielestä vanha mutta edelleen käytetty Asennettava erikseen Pohjalla Qt alustariippumaton rajapintatyökalu ja sidonnat Pythoniin Käytetään tällä kurssilla (toki muitakin saa käyttää mutta tukea ei välttämättä saa) PySide on tämän uudempi versio. Perusasiat pitkälti samoin kummassakin, erona lisenssi PyQt: GPL PySide: LGPL 18:47
(ei-laajan kurssin kalvo: luento 6 sivu 9) Yleiskatsaus Graafinen käyttöliittymä sisältää nappeja, valintaruutuja, valikoita, paneeleja, jne. Jotta voi rakentaa toimivan graafisen käyttöliittymän, tarvitsee tietää: kuinka graafisia komponentteja luodaan Kuinka ne saadaan näkymään ruudulla Kuinka ne saadaan ruudulla sinne, minne halutaan Kuinka ne saadaan tekemään halutuja asioita Graafisia komponentteja luodaan, kuten mitä tahansa Pythonin olioita 18:47
(ei-laajan kurssin kalvo: luento 6 sivu 10) import sys from PyQt4 import QtGui class Example(QtGui.QMainWindow): def init (self): Muistettava kutsua super(example, self). init () yliluokan init self.initui() Ikkunan asetukset Siisti lopetus, kun Mainloop loppuu def initui(self): self.setgeometry(300, 300, 650, 450) self.setwindowtitle('main window') self.show() Tuo ikkuna näkyville def main(): Aina täytyy olla yksi sovellusolio app = QtGui.QApplication(sys.argv) ex = Example() Mahdolliset sys.exit(app.exec_()) komentoriviargumentit if name == ' main ': main() Vasen yläkulma X ja y Komentoriviargumentteja varten Tuodaan graafiset komponentit Tehdään pääikkuna Aloitetaan sovellus ja käynnisteään tapahtumakuuntelija (mainloop) Leveys ja korkeus 18:47
(ei-laajan kurssin kalvo: luento 6 sivu 11) def initui(self): Lisää sisällön exit_action = QtGui.QAction(QtGui.QIcon('./exit.png'), '&Exit', self) exit_action.setshortcut('ctrl+q') exit_action.triggered.connect(qtgui.qapp.quit) menubar = self.menubar() file_menu = menubar.addmenu('&file') file_menu.addaction(exit_action) Aiheuttaa sovelluksen loppumisen self.toolbar = self.addtoolbar('exit') self.toolbar.addaction(exit_action) self.setgeometry(300, 300, 850, 650) self.setwindowtitle('main window') self.show() 'window title' työkalupalkille 18:47
(ei-laajan kurssin kalvo: luento 6 sivu 12) Ikkunan voi periä myös QWidget luokasta mutta silloin työkalu- ja valikkopalkit eivät ole lisättävissä. class Example(QtGui.QWidget): def init (self): super(example, self). init () self.initui() def initui(self): self.button = QtGui.QPushButton("Push", self) self.button.move(30, 50) self.button.clicked.connect(self.button_pressed) Mihin komponenttiin self.setgeometry(300, 300, 290, 150) self.setwindowtitle('event sender') self.show() Liittää nappulan painamisen tapahtumankäsittelijään def button_pressed(self): self.button.settext('pressed') Vaihtaa nappulan tekstin 18:47
(ei-laajan kurssin kalvo: luento 7 sivu 2) Parametrit tapahtumankäsi3elijälle self.clear_bu-on = QtGui.QPushBu-on("Clear", self) self.clear_bu-on.clicked.connect(self.bu-on_pressed) def bu#on_pressed(self): event_message = 'bu-on pressed: clearing fields.' self.clear_texts(event_message) def clear_texts(self, message_text): self.hello_edit.settext('') self.message.settext(message_text) Apufunk=on avulla (tai myös lambda toimii)
Qt:n tapahtumankäsittely: signaalit ja slotit GUI-kirjastojen perusratkaisu tapahtumankäsittelyyn on takaisinkutsu esim. painonappiolion tiettyä metodia kutsutaan, kun nappia painetaan tai painonappiolioon liitetään takaisinkutsufunktio (eli ei välttämättä saman olion metodi) joskus komponenteilla on useampia tapahtumia nappi painetaan alas, nostetaan ylös, klikataan, tuplaklikataan,... Qt on tapahtumissa hieman muita kirjastoja joustavampi: Qt:ssa tapahtuma on signal (nimetty käyttöliittymäolioon liittyvä tapahtuma) takaisinkutsufunktio tms. on slot (toiminto, joka voidaan tehdä, kun jotain tapahtuu) nämä kaksi kytketään (connect) toisiinsa kytkentöjä voi tehdä melko vapaasti: esim. painonapin painamissignaali voi olla kytketty kahteen slottiin, joista kumpikaan ei ole painonappioliossa (molempia kutsutaan)
Signaalit ja slotit: kytkentöjen tekeminen Koodirivejä edellisiltä kalvoilta exit_action.triggered.connect(qtgui.qapp.quit) self.button.clicked.connect(self.button_pressed)... def button_pressed(self):... Primitiivisempi (vanhentunut?) tapa tehdä samat kytkennät QtCore.QObject.connect(exit_action, QtCore.SIGNAL("triggered()"), QtGui.qApp.quit) QtCore.QObject.connect(self.button, QtCore.SIGNAL("clicked()"), self.button_pressed) tässä signaalien nimet ovat triggered ja clicked signaaleilla ja sloteilla voisi olla myös argumentteja C++-kielessä signaalit ja slotit näkyvät eksplisiittisemmin connect:ssa voi pyytää slotin kutsumista joko heti tai jonotetusti ja ohjelma voi helposti tehdä myös omia signaalejaan
Sisältö 1 GUI-ohjelmointia Qt:lla, tapahtumankäsittely 2 Komponenttien asettelu 3 Sekalaista GUI-ohjelmoinnista 4 Omien komponenttien piirtäminen
Komponenttien asettelu käyttöliittymäelementit sijoitetaan paikalleen yleensä asetteluolioiden (esim. QGridLayout) avulla usein niitä käytetään sisäkkäin suorien pikselikoordinaattien käyttämistä kannattaa välttää ikkunan skaalaamisen lisäksi mm. käännökset eri kielille, fontin ja ulkoasutyylin muutokset eivät toimi kunnolla asetteluoliot tekevät ikkunasta skaalattavan itse pitää päättää, mitä ikkunan osista skaalataan (säädetään asetteluolioiden parametreilla) ikkunan sisälle saa myös rajoja käyttäjän siirrettäväksi kannattaa joskus selata läpi esim. QGridLayoutin metodit
(ei-laajan kurssin kalvo: luento 6 sivu 13) def initui(self): self.label = QtGui.QLabel("Say hello:", self) self.hello_edit = QtGui.QLineEdit(self) self.clear_button = QtGui.QPushButton("Clear", self) self.clear_button.clicked.connect(self.button_pressed) self.message = QtGui.QLabel('message label', self) self.hello_edit.textedited.connect(self.onedited) #Horizontal layout h_box1 = QtGui.QHBoxLayout() h_box1.addwidget(self.label) h_box1.addwidget(self.hello_edit) h_box1.addwidget(self.clear_button) h_box2 = QtGui.QHBoxLayout() h_box2.addwidget(self.message) #vertical layout v_box = QtGui.QVBoxLayout() v_box.addlayout(h_box1) v_box.addlayout(h_box2) self.setlayout(v_box) self.setgeometry(300, 300, 890, 650) self.setwindowtitle('layout example') self.show() def button_pressed(self): self.hello_edit.settext('') self.message.settext('') def onedited(self, text): self.message.settext(text) self.message.adjustsize() Jos tekstirivi muuttuu, mennään metodiin onedited Vaakatason layout komponentti Toiselle riville samanlainen Laitetaan ne päällekkäin pystysuunnan layout-komponenttiin Pystysuunnan komponentti määrää Ikkunan asettelun 18:47
(ei-laajan kurssin kalvo: luento 6 sivu 14) Useamman rivin tekstikenttä Luodaan ruudukko def initui(self): grid = QtGui.QGridLayout() rivi sarake grid.addwidget(qtgui.qlabel('one'), 0, 0) grid.addwidget(qtgui.qlabel('two'), 0, 1) grid.addwidget(qtgui.qlabel('three'), 0, 2) grid.addwidget(qtgui.qlabel('four'), 1, 0) grid.addwidget(qtgui.qlabel('five'), 1, 2) grid.addwidget(qtgui.qlabel('six'), 2, 0) grid.addwidget(qtgui.qlabel('seven'), 2, 1) grid.addwidget(qtgui.qtextedit(), 3, 0, 2, 3) self.setlayout(grid) Ikkunan asettelu ruudukkona self.resize(300, 200) self.move(300, 150) self.setwindowtitle('grid example') self.show() Lisätään ruudukkoon komponentteja Useamman ruudun kattava komponentti Monelle riville ja sakkeelle Alkuruutu: rivi, sarake 18:47
Käyttöliittymänsuunnittelutyökalut suurissa käyttöliittymäkirjastoissa on oma työkalu, jolla käyttöliittymiä voi piirtää graasesti esim. Qt:ssa QtDesigner, GTK:ssa Glade työkalu tekee joko (pohja)koodin, joka luo käyttöliittymäoliot, tai kuvauskieltä, jota koodi osaa lukea generoidun pohjakoodin muuttaminen on hankalaa: silloin käyttöliittymää ei enää voi muokata työkalun avulla jotkin suunnittelutyökalut antavat kirjoittaa osia pohjakoodista ja joskus pohjakoodi toimii sellaisenaan kuvauskielen käyttämisellä välttää nämä ongelmat, mutta käyttöliittymäolioihin viittaminen vaatii vähän omaakin koodia isommissa käyttöliittymissä kannattaa usein käyttää työkalua opettelua varten ja pienissä ohjelmissa kannattanee kirjoittaa käyttöliittymäkoodi kokonaan itse
(ei-laajan kurssin kalvo: luento 6 sivu 15) Muutama muistettava Jos periytät ikkunasi QMainWindow-luokasta, täytyy siihen luoda CentralWidget, joka pitää sisällään kaikki ikkunan komponentit. Vaikka näin: self.main_widget = QtGui.QWidget(self) self.setcentralwidget(self.main_widget) okbutton = QtGui.QPushButton("OK", self.main_widget) Jos tarvii, niin komponentteja voi asetella myös pakotetusti annettuun kohtaan ikkunassa: lbl1 = QtGui.QLabel('Laput', self) lbl1.move(15, 10) lbl2 = QtGui.QLabel('pysyvät', self) lbl2.move(35, 40) Ikkunan kokoa muutettaessa nämä eivät siirry 18:47
Sisältö 1 GUI-ohjelmointia Qt:lla, tapahtumankäsittely 2 Komponenttien asettelu 3 Sekalaista GUI-ohjelmoinnista 4 Omien komponenttien piirtäminen
Vielä yksi esimerkki 1/2 Oma käyttöliittymäolio + ajastin import time from PyQt4 import QtCore, QtGui qtclock.py class Clock(QtGui.QLCDNumber): def init (self): super(clock, self). init () self.setframestyle(qtgui.qframe.panel QtGui.QFrame.Raised) self.setsegmentstyle(qtgui.qlcdnumber.filled) self.setdigitcount(8) self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.updatetime) self.timer.start(1000) self.updatetime() @QtCore.pyqtSlot() ; kertoo että metodi on slot (ei pakollinen) def updatetime(self): text = time.strftime("%h:%m:%s", time.localtime()) self.display(text) tässä peritään valmis olio QLCDNumber ja muokataan sitä vähän QTimer tekee ajastetun tapahtuman (tässä toistuvan)
Vielä yksi esimerkki 2/2 Ikkuna ja pääohjelma class ClockDialog(QtGui.QDialog): def init (self): super(clockdialog, self). init () qtclock.py clock = Clock() button = QtGui.QPushButton("&Quit") button.clicked.connect(self.close) layout = QtGui.QVBoxLayout() layout.addwidget(clock) layout.addwidget(button) self.setlayout(layout) self.resize(300, 150) self.setwindowtitle('clock') if name == ' main ': import sys app = QtGui.QApplication(sys.argv) clockdialog = ClockDialog() sys.exit(clockdialog.exec_())
(ei-laajan kurssin kalvo: luento 6 sivu 8) Mistä löytää tietoa Alkuun pääsee lukemalla seuraavan: http://zetcode.com/tutorials/pyqt4/ Tai http://www.learningpython.com/2008/09/20/an-introduction-to-pyqt/ PyQt:n luokkakuvaukset http://www.riverbankcomputing.com/static/docs/pyqt4/html/classes.html 18:47
Mitkä graaset käyttöliittymät ovat tällaisia? melkein kaikki graaset käyttöliittymäkirjastot käyttävät samankaltaista oliomallia jopa kielissä, jotka eivät tue olioita (esim. C ja GTK) siellä olioita simuloidaan kuten Scheme-hissitehtävässä oliomallin rakenne on myös useimmiten hyvin samanlainen komponentteja, säiliöitä, sijoitteluasetuksia, painalluksiin ym. tapahtumiin reagoimista, jne. komponentitkin ovat enimmäkseen samoja, joskin eri näköisiä vaihtoehtoisia malleja (harvinaisia): matalamman tason malli: kirjasto tarjoaa vain piirtoalustan, ei komponentteja (esim. Pygame eli SDL, OpenGL) lomakemalli: esim. WWW-sivu tarjoaa lomakkeen ja saa sen täytettynä selaimelta (Web-sovelluksille tämä ei yleensä riitä) muitakin (esim. funktionaalisia) GUI-malleja on esitetty, mutta lähinnä tutkimusmielessä
Sisältö 1 GUI-ohjelmointia Qt:lla, tapahtumankäsittely 2 Komponenttien asettelu 3 Sekalaista GUI-ohjelmoinnista 4 Omien komponenttien piirtäminen
(ei-laajan kurssin kalvo: luento 7 sivu 3) Käy3ölii3ymän piirtäminen QtGui.QPainter Kaavioiden tekoon Käyrien piirtelyyn BiHkar3akuvien tekoon Piirtäminen tapahtuu korvaamalla paintevent- metodi Sitä kutsutaan aina automaahses=, kun ikkuna pitää piirtää Alussa Koko muu3uu
(ei-laajan kurssin kalvo: luento 7 sivu 4) import sys from PySide import QtGui, QtCore class Example(QtGui.QWidget): def init (self): super(example, self). init () self.initui() def initui(self): self.setgeometry(300, 300, 280, 170) self.setwindowtitle('kuvioita') self.show() def main(): app = QtGui.QApplica=on(sys.argv) ex = Example() sys.exit(app.exec_()) if name == ' main ': main() def paintevent(self, e): painter = QtGui.QPainter() painter.begin(self) self.draw_circle(painter) self.draw_line(painter) self.draw_rectangle(painter) painter.end() def draw_circle(self, painter): painter.setpen(qtcore.qt.green) painter.drawellipse(100, 50, 50, 50) def draw_line(self, painter): pen = QtGui.QPen(QtCore.Qt.red) pen.setwidth(4) painter.setpen(pen) painter.drawline(10, 30, 250, 150) def draw_rectangle(self, painter): painter.setpen(qtcore.qt.yellow) color = QtGui.QColor(QtCore.Qt.darkYellow) painter.setbrush(color) painter.drawrect(130, 120, 250, 150)
(ei-laajan kurssin kalvo: luento 7 sivu 5) def paintevent(self, e): painter = QtGui.QPainter() painter.begin(self) self.draw_circle(painter) self.draw_line(painter) self.draw_rectangle(painter) painter.end() def draw_circle(self, painter): painter.setpen(qtcore.qt.green) painter.drawellipse(100, 50, 50, 50) def draw_line(self, painter): pen = QtGui.QPen(QtCore.Qt.red) pen.setwidth(4) painter.setpen(pen) painter.drawline(10, 30, 250, 150) Luo painter alku def draw_rectangle(self, painter): painter.setpen(qtcore.qt.yellow) painter.setbrush(qtgui.qcolor(qtcore.qt.darkyellow)) painter.drawrect(130, 120, 250, 150) Kaikki piirtäminen noi3en väliin Metodeja kanna3aa käy3ää piirtoväri Laa=kon vasen ylänurkka, leveys, korkeus Viivan leveys loppu
Huomioita Komponen=n taustaväri: setbrush()- metodilla Värit Qt- väreinä tai RGB Myöhemmin piirre3y pei3ää aina aiemmat (ei-laajan kurssin kalvo: luento 7 sivu 6)
Mitä painteventin pitää tehdä? GUI-kirjastoissa piirtämistä ei yleensä voi tehdä vain kutsumalla piirroskomentoja sieltä täältä muualta ohjelmasta sillä ohjelman pitää milloin tahansa osata piirtää ikkunansa uudelleen käytännössä paintevent-metodia kutsutaan esim. kun käyttäjä tuo ikkunan näkyviin toisen ikkunan alta käyttöliittymäkirjasto ei siis (yleensä) talleta piirrettyä kuvaa mihinkään itse joissain käyttöliittymäkirjastoissa voidaan myös pyytää piirtämään uudelleen vain osa komponentista yleensä paintevent tehdään piirtämään ikkuna muualla ohjelmassa tallessa olevien ohjeiden mukaan poikkeus: jos kuva vaihtuu tiheään (esim. reaaliaikainen peli, jonka kuva päivittyy monta kertaa sekunnissa), ei välttämättä ole hyötyä piirtää vanhaa kuvaa uudelleen
(ei-laajan kurssin kalvo: luento 7 sivu 7) Muita vaihtoehtoja QGraphicsScene + QGraphicsView KomponenHen tapahtumia voi seurata ja niitä voi siirrellä toisistaan riippuma3a Komponen=t helppo paikantaa Scene säilöö komponen=t View näy3ää ne Useampi View voi näy3ää näkymää eri kohdista
(ei-laajan kurssin kalvo: luento 7 sivu 8) QGraphicsScene (näkymä) QGraphicsItem QGraphicsView (näköala)
(ei-laajan kurssin kalvo: luento 7 sivu 9) class MainWindow(QtGui.QMainWindow): def init (self): self.add_view() clear_ac=on.triggered.connect(self.clear_view) def clear_view(self): self.scene.clear() def add_view(self): self.view = MyView() Luo näköala Luo näkymä self.scene = QtGui.QGraphicsScene() Liitä toisiinsa self.view.setscene(self.scene) self.view.setscenerect(qtcore.qrectf(self.rect())) Näköala asetetaan koko ikkunaksi self.setcentralwidget(self.view)
(ei-laajan kurssin kalvo: luento 7 sivu 10) class MyView(QtGui.QGraphicsView): def init (self): super(myview, self). init () Halutaan käy3ää näkymän koordina3eja def mousemoveevent(self, event): end_pos = self.maptoscene(event.pos()) line = QtCore.QLineF(self.current_pos, end_pos) self.scene().addline(line) self.current_pos = end_pos def mousepressevent(self, event): self.current_pos = self.maptoscene(event.pos()) Lisätään näkymään viiva, Joka alkaa edelllisen loppupisteestä Otetaan talteen uuden viivasöherön alkupiste