Kirjautuminen

Haku

Tehtävät

Keskustelu: Yleinen keskustelu: Suositeltava käyttöliittymän ohjelmointityyli Pythonilla

Sivun loppuun

kayttaja-15306 [12.12.2020 08:16:09]

#

Tervehdys,

yritän opetella suositeltavaa graafisen käyttöliittymän ohjelmointityyliä Pythonilla seuraavan linkin ohjeiden mukaan:

https://www.begueradj.com/tkinter-best-practices/

Käytössä on versio 3.8.

Linkin esimerkissä ei täydennetty mitään esimerkkiä create_widgets() metodille/funtiolle ja kysyisin, mitä siihen pitäisi lisätä, että se luo käyttöliittymän seuraavassa yksinkertaisessa sovelletussa koodissa:

import tkinter as tk
class MainApplication(tk.Frame):
    def __init__(self, master):
        self.master = master
        tk.Frame.__init__(self, self.master)

        self.frame_left = tk.Frame
        self.frame_right = tk.Frame

        self.configure_gui()
        self.create_widgets()

    def configure_gui(self):
        self.master.title("Example GUI")
        self.master.geometry("500x500")
        self.master.resizable(True, True)

        self.frame_left(height=250, width=250)
        self.frame_right(height=250, width=250)
        self.frame_left.grid(self, row=0, column=0)
        self.frame_right.grid(self, row=0, column=1)
        self.frame_left.configure(self, bg='black')
        self.frame_right.configure(self, bg='yellow')

    def create_widgets(self):

Eli mitä tänne pitää lisätä on se kysymys.

if __name__ == '__main__':

    root = tk.Tk()
    main_app =  MainApplication(root)
    root.mainloop()

The Alchemist [12.12.2020 10:06:37]

#

No mitä sun mielestä tarvitsee lisätä, jotta saat käyttöliittymän aikaiseksi? Jos esimerkiksi haluat ruudulle tekstilaatikon ja napin, niin mitä koodia sen eteen on kirjoitettava?

Itse valitsisin harjoittelualustaksi sellaisen toolkitin, jolla näitä käyttöliittymiä voi "piirrellä" graafisella työkalulla. Vähänkään monimutkaisemman kälin luominen karkaa käsistä, jos sitä yrittää tehdä pelkästään käsin koodaamalla.

Qt:lle sekä GTK:lle on ainakin olemassa graafisia työkaluja; Qt Designer ja Glade. Qt soveltuu käytettäväksi Windowsilla, Linuxilla ja Macilla; GTK taitaa nykyään olla Linux-orientoitunut.

Metabolix [12.12.2020 10:08:47]

#

Kyseinen sivu kertoo kirjoittajan suosittelemasta koodin rakenteesta, ei varsinaisesti Tkinterin käytöstä tai käyttöliittymän tekemisestä. Tkinter-oppaita löytyy netistä monia, etsi mieleisesi. (Koodissasi on myös sellainen paha vika, että et luo frameja vaan sijoitat tk.Frame-luokan muuttujiin. Selvästi pitää aloittaa opettelu ihan alkeista.)

kayttaja-15306 [12.12.2020 10:45:06]

#

Tervehdys,

kiitoksia vastauksista. Opittavaa on varmasti paljonkin sen myönnän aluliisti.

Käytännössä haluaisin tehdä yksinkertaisen käyttöliittymän ja saada sen näytölle esiin, jolla saan esin. ne pari määriteltyä frame ikkunaa näytölle esiin ja voisin lisätä tähän koodiin sitten muita käyttöliittymän elementtejä kuten esim. teksti-ikkunan niiden päälle ja painikkeita joillekin yksinkertaisille toiminnoille.

Tähän saakka olen tehnyt ohjelmat yhden ja saman app-luokan sisälle ja niiden alle funtiot, joita ohjelmassa tarvitaan. Tämä ei liene paras mahdollinen koodinrakenne käyttöliittymän rakentamisessa, vaikka sinänsä toimiikin ok.

kayttaja-15306 [12.12.2020 11:27:41]

#

Tervehdys,

tässä pieni käyttöliittymäesimerkki tavasta, jolla normaalisti teen käyttöliittymän. Mitä vikaa tässä tavassa on?

import tkinter as tk
from tkinter import scrolledtext

class MyApp:

    def __init__(self, parent):

        self.myParent = parent
        parent.title ('Example GUI')
        parent.configure(bg='#c0c0c0')

        self.frm_main = tk.Frame(parent)
        self.frm_main.grid(row=1, column=0, padx=50)
        self.frm_main.configure(bg='#c0c0c0', borderwidth=5)
        self.frm_main.columnconfigure(0, weight=1)
        self.frm_main.grid_columnconfigure(0, weight=1)

        self.frm_left = tk.Frame(self.frm_main, width=420, height=270)
        self.frm_left.grid(row=0, column=0, sticky='ewns')
        self.frm_left.configure(bg='gray80')
        self.frm_left.grid_columnconfigure(0, weight=1)

        self.frm_right = tk.Frame(self.frm_main, width=420, height=270)
        self.frm_right.grid(row=0, column=1, sticky='ewnw')
        self.frm_right.configure(bg='#4267b2')
        self.frm_right.grid_columnconfigure(0, weight=1)

        self.frm_middle = tk.Frame(parent)
        self.frm_middle.grid(row=2, column=0)
        self.frm_middle.configure(bg='#c0c0c0', borderwidth=10)
        self.frm_middle.grid_columnconfigure(0, weight=1)
        self.frm_middle.grid_rowconfigure(0, weight=1)

        self.text=scrolledtext.ScrolledText(parent, width=60, height=10,
        wrap='word', spacing1=1, spacing2=1, spacing3=1)
        self.text.grid(row=1, column=0, padx=15, pady=15)
        self.text.config(bg='light blue', font=('Verdana', 14),
        borderwidth=2, relief='ridge', foreground='black', state='normal')
        self.text.grid_columnconfigure(0, weight=1)
        self.text.grid_rowconfigure(0, weight=1)
        self.text.grid_propagate(0)


root = tk.Tk()
app = MyApp(root)
root.mainloop()

Metabolix [12.12.2020 12:02:36]

#

Kuten artikkelissa selitetään, olennainen ero on koodin järjestely ja luettavuus. Artikkelissa on annettu esimerkki jaosta: configure_gui sisältää ikkunan yleiset asetukset, ja käyttöliittymän eri osille on omat funktionsa kuten create_menu ja create_game_board. Jos teet 100 erilaista nappia ja tekstiä tuolla nykyisellä tyylilläsi, on vaikea löytää koodista tiettyä kohtaa. Useaan funktioon jaetusta koodista oikean kohdan löytää helpommin, kun tietää jo valmiiksi, että esimerkiksi valikkoon liittyvät asiat ovat funktiossa create_menu.

Yleensäkin ihan aiheesta riippumatta kannattaa jakaa koodi funktioihin, joissa jokaisessa tehdään jokin järkevä kokonaisuus.

kayttaja-15306 [12.12.2020 13:01:03]

#

Tervehdys,

kiitos vastauksesta. Funktioden käyttö käyttöliittymän eri osille on vielä opeteltavien asioiden joukossa ja niiden käyttö varmaankin, varsinkin isommassa ohjelmassa, selkiyttää koodia.

Artikkelin esimerkistä ymmärsin konfigurointifuntion sinänsä, mutta miten artikkelissa mainittu create_widgets() funktion toiminta jäi epäselväksi eli miten sitten luodaan ne eri käyttöliittymän osat yhdeksi kokonaisuudeksi.

kayttaja-15306 [12.12.2020 13:17:08]

#

Tervehdys,

Olisiko liikaa pyydetty Metabolix, että teksit viimeksi lähettämästäni koodista konkreettisen koodiesimerkin, miten frame ja text widgeteille tehtäisiin omat funktionsa ja miten niitä kutsuttaisiin create_widgets() funkiossa siten, että ohjelma toimii kuten esimerkissänikin?

Luultavasti osaisin tämän rungon avulla lisätä muita widgettejä tähän peruskoodiin itsekin, mutta parhaiten tuntuu oppivan konkreettisen oman sovellusesimerkin avulla.

Metabolix [12.12.2020 14:42:57]

#

Kai kuitenkin osaat tehdä funktioita? Jos et, aloita lukeminen ihan tavallisesta Python-oppaasta.

Käyttöliittymän jako funktioihin on erittäin yksinkertaista. Nykyisessä koodissasi teet kaiken funktiossa __init__:

class X:
	def __init__(self, parent):
		koodia_1(foo)
		koodia_2(bar)
		koodia_3(baz)

Sinun pitää vain siirtää osia koodista muihin funktioihin ja kutsua niitä oikeassa järjestyksessä:

class X:
	def __init__(self, parent):
		self.configure_gui()
		self.create_jotain_fiksua()
		self.create_jotain_muuta_fiksua()

	def configure_gui(self):
		koodia_1(foo)

	def create_jotain_fiksua(self):
		koodia_2(bar)

	def create_jotain_muuta_fiksua(self):
		koodia_3(baz)

En pysty tuosta puutteellisesta koodistasi sanomaan, mitkä asiat juuri sinun kohdallasi kuuluisi laittaa tiettyyn funktioon. Kuitenkin olisi järkevää, että yhdessä funktiossa olisi aina selvästi samaan asiaan liittyviä osia. Periaatteessa voit tehdä erillisiä funktioita vaikka noista palasista, jonka nyt olet erottanut tyhjällä rivillä.

kayttaja-15306 [12.12.2020 17:36:28]

#

Tervehdys,

kiitos vastauksesta. Osaan toki tehdä funktioita itsekin.

Käytännössä funktion ei tarvitse tehdä mitään ihmeellistä, esim. vain korottaa luku potensiin 2, jotta näkisi vain miten ohjelma toimii tällä tavoin koodattuna.

The Alchemist [13.12.2020 07:23:35]

#

Widgettien kanssa pätevät kaikki yleiset olio-ohjelmoinnin prinsiipit. Mikäli joukko widgettejä muodostaa kiinteän kokonaisuuden, niin kannattaa kääriä ne uuteen luokkaan (joka on tyhjä widgetti), jolloin kyseisen joukon toiminnot on eristetty omaan kapseliinsa eikä koodi sekoitu muuhun koodiin.

Sinulle on varmasti tuttu jo se periaate, että (konsoliin) tulostamista ja laskentaa ei pitäisi sekoittaa samaan funktioon (tai luokkaan). Tämä periaate pätee totta kai myös GUI-tasolla, eli se luokka, joka hallitsee widgettejä, tuskin osallistuu sovelluksen logiikan tuottamiseen. Ei, se luokka tarvitsee vain jonkin tekstin tai numeron, joka tulee tuottaa ruudulle, sen ei tule välittää siitä, millaisen laskennan tuloksena teksti tai numero on saatu.

Tässä vaikuttaa olevan nyt ongelmana vähän se, ettet suostu kuuntelemaan ja miettimään, mitä sinulle kerrotaan, vaan kaikki pitäisi saada valmiiksi pureskeltuna. Millekään toolkitille ei ole yhtä tiettyä totuutta, jonka mukaisesti pitäisi ohjelmoida. Sinun tulee itse etsiä sopivat tavat erilaisten asioiden toteuttamiseksi.

Alla karkea esimerkki siitä, miten yksinkertainen laskin voitaisiin tehdä. Koodi on jotakuinkin pythonia mutta se ei tietenkään toimi sinällään. Tarkoitus on havainnollistaa graafisen käyttöliittymän ja sovelluslogiikan välistä toimintaa ja erottelua.

class MainWindow:
    def __init__(self):
        self.__setup()
        self.__gui()
        self.calculator = Calculator()

    def __setup(self):
        ##
        # Alusta sovelluksen perustila tms.
        #

        self.expression = []

    def __gui(self):

        self.layout = VerticalLayout()

        # Laskimen näyttö eli jokin syötelaatikko tai label.
        self.display = TextBox()

        # Itse koostettu numeronäppäimistö.
        self.numpad = Numpad()

        self.layout.add(display)
        self.layout.add(numpad)

        ##
        # Tässä "input" on ns. signaali eli jokin eventin lähde, joka kutsuu oman luokkamme
        # metodia ja välittää signaalimetodille annetun argumentin.
        #
        self.numpad.input.connect(self.__on_input)

    def __on_input(self, symbol):
        self.expression.push(symbol)

        if self.calculator.validate(self.queue):
            self.do_computation(self.expression)

    def do_computation(self, expression):
        result = self.calculator.compute(expression)
        self.display.text = "{0} = {1}".format(" ".join(expression), result)

class Numpad:
    def __init__(self):
        self.grid = GridLayout()
        self.buttons = [Button("1"), Button("2"), Button("3"), ...]

        self.__add_buttons_to_grid(self.buttons)
        self.__create_button_listeners(self.buttons)

    def __add_buttons_to_grid(self, buttons):
        for button in buttons:
            self.grid.add(button)

    def __create_button_listeners(self, buttons):
        for button in buttons:
            ##
            # Lähetetään "signaali", jota luokan ulkopuolella liitetyt käsittelijät voivat kuunnella.
            #
            button.click.connect(lambda: self.input.emit(button.text))

class Calculator:
    def __init__(self):
        self.validator = ExpressionValidator()

    def validate(self, expression):
        self.validator.validate(expression)

    def compute(self, expression):
        self.validate(expression)

        # Osataan käsitellä vain pluslaskut tällä hetkellä.
        return int(expression[0]) + int(expression[2])

class ExpressionValidator:
    def validate(expression):
        int(expression[0])
        int(expression[2])

        if expression[1] not in "+-/*":
            raise ValueError("Invalid operator")

The Alchemist [13.12.2020 08:29:00]

#

Ja tärkein asia muistaa on se, että jos opettelet vain yhden tavan yhden asian tekemiseen, ja sen jälkeen aivottomasti toistat sitä tasan joka paikassa, niin koodisi tulee olemaan paskaa. Sinun on täysin turhaa haaskasta aikaa obsessoidaksesi tällaisesta merkityksettömästä asiasta.

kayttaja-15306 [13.12.2020 08:45:42]

#

Tervehdys,

kiitoskia vastauksista.

Yritän sisäistää asioita jatkossa itse paremmin ilman, että jonkun pitäisi tehdä siitä valmis ratkaisu. Päätetään asian käsittely tähän tämän asian osalta.

Metabolix [13.12.2020 10:29:02]

#

ma08 kirjoitti:

Käytännössä funktion ei tarvitse tehdä mitään ihmeellistä, esim. vain korottaa luku potensiin 2, jotta näkisi vain miten ohjelma toimii tällä tavoin koodattuna.

Voisitko selittää selvemmin, mitä vielä toivot? Miten tällainen funktio (luku potenssiin 2) auttaisi ymmärtämään käyttöliittymän ohjelmointia? Mitä haluat nähdä aiheesta ”miten ohjelma toimii tällä tavoin koodattuna”? Koko ideahan on se, että ohjelma toimii täsmälleen samalla tavalla kuin ennenkin ja koodia vain järjestellään uudelleen.

Edellisessä esimerkissäni jo kuvasin, miten koodin voi jakaa useaan funktioon: tee uusi funktio, siirrä osa koodiriveistä sinne ja laita alkuperäiseen kohtaan tilalle funktiokutsu. Tämä varmaan oli jo ennestään selvää, kun kerran tiedät jo funktioista.

Ja kuten myös sanoin, voit aivan itse valita, mitkä rivit laitat uuteen funktioon. Sinun koodissasi ei ole mitään toimivaa käyttöliittymää tai ohjelmaa vaan ainoastaan muutama tyhjä laatikko, joten kukaan ei pysty sinua auttamaan siinä, miten nuo tyhjät laatikot pitäisi toteuttaa ”paremmin”. Ehdotin jo yleispätevänä vaihtoehtona, että laittaisit jokaisen tyhjällä rivillä erotellun osuuden (eli käytännössä jokaisen widgetin) omaksi funktiokseen. Isommassa ohjelmassa on syytä tehdä myös useampia luokkia, kuten The Alchemistin periaatteellisessa esimerkissä.

Jos asia jäi jotenkin epäselväksi tai toteutuksessa on jokin vaikeus, ehkä voisit muotoilla kysymyksen täsmällisemmin.

kayttaja-15306 [13.12.2020 12:16:39]

#

Tervehdys,

kiitokset kommenteista kaikille asiassa vaivaa nähneille.

Sain itse asiassa jo järjetettyä erään aikemmin tekemäni ohjelman käyttöliittymän koodin erillisiksi funktioksi ja se tuntuisi jopa toimivankin.

Käyttöliittymäkoodinkin jäsentäminen tällä tavalla selkiyttää todellakin ohjelman rakennetta ja aion käyttää sitä jatkossa muissakin ohjelmissa.

The Alchemist [13.12.2020 14:20:42]

#

Metabolix kirjoitti:

Isommassa ohjelmassa on syytä tehdä myös useampia luokkia, kuten The Alchemistin periaatteellisessa esimerkissä.

Sitten kun osaa oikeasti osaa ohjelmoida eli hahmottaa ohjelman perusrakenteen jo ennen ensimmäistäkään koodiriviä, niin mielestäni tällaiset asialliset luokkajaot tulevat itsestään, ei niitä tarvitse erikseen miettiä eikä yrittää. Enkä näe mitään syytä, miksei jo yksinkertaisemmatkin sovellukset kannattaisi pilkkoa selkeisiin eristettyihin komponentteihin.

Luokkajaon opettelu onnistuu parhaiten yksinkertaisilla mutta kuitenkin täysin toiminnallisilla "sovelluksilla". Jos sellaista lähtee kokeilemaan ensimmäisen kerran monimutkaisemman sovelluksen kohdalla, niin saattaa helposti lähteä tekemään vääränlaisia luokkia ja silloin koodista tulee toivotonta spagettia.

kayttaja-15306 [13.12.2020 16:22:00]

#

Tervehdys,

olisiko tiedosssa jotain hyvää olio-ohjelmoinnin (alkesi)oppikirjaa joko suomeksi tai englanniksi?


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta