sql >> Database teknologi >  >> RDS >> Database

Håndtering af SQL-databaser med PyQt:Det grundlæggende

At bygge applikationer, der bruger en SQL-database, er en ret almindelig programmeringsopgave. SQL-databaser er overalt og har stor support i Python. I GUI-programmering giver PyQt robust og cross-platform SQL-databaseunderstøttelse, der giver dig mulighed for at oprette, oprette forbindelse til og administrere dine databaser konsekvent.

PyQts SQL-understøttelse integreres fuldt ud med dens Model-View-arkitektur for at hjælpe dig i processen med at bygge databaseapplikationer.

I dette selvstudie lærer du, hvordan du:

  • Brug PyQts SQL-understøttelse for pålidelig forbindelse til en database
  • Udfør SQL-forespørgsler på en database ved hjælp af PyQt
  • Brug PyQts Model-View-arkitektur i databaseapplikationer
  • Vis og rediger data ved hjælp af forskellige PyQt widgets

Eksemplerne i denne vejledning kræver et grundlæggende kendskab til SQL-sproget, især til SQLite-databasestyringssystemet. Noget tidligere kendskab til GUI-programmering med Python og PyQt vil også være nyttigt.

Gratis bonus: 5 Thoughts On Python Mastery, et gratis kursus for Python-udviklere, der viser dig køreplanen og den tankegang, du skal bruge for at tage dine Python-færdigheder til næste niveau.


Tilslutning af PyQt til en SQL-database

At forbinde en applikation til en relationsdatabase og få applikationen til at oprette, læse, opdatere og slette de data, der er gemt i den database, er en almindelig opgave i programmering. Relationelle databaser er generelt organiseret i et sæt tabeller eller relationer . En given række i en tabel omtales som en post eller tuppel , og en kolonne omtales som en attribut .

Bemærk: Udtrykket felt bruges almindeligvis til at identificere et enkelt stykke data, der er gemt i en celle i en given post i en tabel. På den anden side udtrykket feltnavn bruges til at identificere navnet på en kolonne.

Hver kolonne gemmer en bestemt type information, såsom navne, datoer eller tal. Hver række repræsenterer et sæt af tæt relaterede data, og hver række har den samme generelle struktur. For eksempel, i en database, der gemmer data om medarbejderne i en virksomhed, repræsenterer en bestemt række en individuel medarbejder.

De fleste relationelle databasesystemer bruger SQL (struktureret forespørgselssprog) til at forespørge, manipulere og vedligeholde dataene i databasen. SQL er et deklarativt og domænespecifikt programmeringssprog, der er specielt designet til at kommunikere med databaser.

Relationelle databasesystemer og SQL er meget udbredt i dag. Du finder flere forskellige databasestyringssystemer, såsom SQLite, PostgreSQL, MySQL, MariaDB og mange andre. Du kan forbinde Python til ethvert af disse databasesystemer ved hjælp af et dedikeret Python SQL-bibliotek.

Bemærk: Selvom PyQts indbyggede SQL-understøttelse er den foretrukne mulighed for at administrere SQL-databaser i PyQt, kan du også bruge et hvilket som helst andet bibliotek til at håndtere databaseforbindelsen. Nogle af disse biblioteker inkluderer SQLAlchemy, pandaer, SQLite og så videre.

Men at bruge et andet bibliotek til at administrere dine databaser har nogle ulemper. Du vil ikke være i stand til at drage fordel af integrationen mellem PyQts SQL-klasser og Model-View-arkitekturen. Derudover vil du tilføje ekstra afhængigheder til din applikation.

Når det kommer til GUI-programmering med Python og PyQt, giver PyQt et robust sæt klasser til at arbejde med SQL-databaser. Dette sæt klasser vil være din bedste allierede, når du skal forbinde din applikation til en SQL-database.

Bemærk: Desværre har PyQt5s officielle dokumentation nogle ufuldstændige sektioner. For at omgå dette kan du tjekke PyQt4-dokumentationen, Qt For Pythons dokumentation eller den originale Qt-dokumentation. I denne øvelse fører nogle links dig til den originale Qt-dokumentation, som i de fleste tilfælde er en bedre informationskilde.

I denne øvelse lærer du det grundlæggende i, hvordan du bruger PyQts SQL-understøttelse til at skabe GUI-applikationer, der pålideligt interagerer med relationelle databaser for at læse, skrive, slette og vise data.


Oprettelse af en databaseforbindelse

At forbinde dine applikationer med en fysisk SQL-database er et vigtigt skridt i processen med at udvikle databaseapplikationer med PyQt. For at udføre dette trin med succes, har du brug for nogle generelle oplysninger om, hvordan din database er sat op.

For eksempel skal du vide, hvilket databasestyringssystem din database er bygget på, og du skal muligvis også have et brugernavn, en adgangskode, et værtsnavn og så videre.

I denne tutorial skal du bruge SQLite 3, som er et gennemtestet databasesystem med support på alle platforme og minimale konfigurationskrav. SQLite giver dig mulighed for at læse og skrive direkte til databaser på din lokale disk uden behov for en separat serverproces. Det gør det til en brugervenlig mulighed for at lære udvikling af databaseapplikationer.

En anden fordel ved at bruge SQLite er, at biblioteket leveres med Python og også med PyQt, så du ikke behøver at installere andet for at begynde at arbejde med det.

I PyQt kan du oprette en databaseforbindelse ved at bruge QSqlDatabase klasse. Denne klasse repræsenterer en forbindelse og giver en grænseflade til at få adgang til databasen. For at oprette en forbindelse skal du blot kalde .addDatabase()QSqlDatabase . Denne statiske metode tager en SQL-driver og et valgfrit forbindelsesnavn som argumenter og returnerer en databaseforbindelse:

QSqlDatabase.addDatabase(
    driver, connectionName=QSqlDatabase.defaultConnection
)

Det første argument, driver , er et påkrævet argument, der indeholder en streng, der indeholder navnet på en PyQt-understøttet SQL-driver. Det andet argument, connectionName , er et valgfrit argument, der indeholder en streng med navnet på forbindelsen. connectionName standard til QSqlDatabase.defaultConnection , som normalt indeholder strengen "qt_sql_default_connection" .

Hvis du allerede har en forbindelse kaldet connectionName , så fjernes den forbindelse og erstattes med en ny forbindelse, og .addDatabase() returnerer den nyligt tilføjede databaseforbindelse tilbage til den, der ringer.

Et kald til .addDatabase() tilføjer en databaseforbindelse til en liste over tilgængelige forbindelser. Denne liste er et globalt register at PyQt vedligeholder bag kulisserne for at holde styr på de tilgængelige forbindelser i en applikation. Registrering af dine forbindelser med et meningsfuldt connectionName giver dig mulighed for at administrere flere forbindelser i en databaseapplikation.

Når du har oprettet en forbindelse, skal du muligvis angive flere attributter på den. Det specifikke sæt attributter vil afhænge af den driver, du bruger. Generelt skal du angive attributter såsom databasenavnet, brugernavnet og adgangskoden for at få adgang til databasen.

Her er en oversigt over indstillingsmetoderne, som du kan bruge til at indstille de mere almindeligt anvendte attributter eller egenskaber for en databaseforbindelse:

Metode Beskrivelse
.setDatabaseName(name) Indstiller databasenavnet til name , som er en streng, der repræsenterer et gyldigt databasenavn
.setHostName(host) Indstiller værtsnavnet til host , som er en streng, der repræsenterer et gyldigt værtsnavn
.setUserName(username) Indstiller brugernavnet til username , som er en streng, der repræsenterer et gyldigt brugernavn
.setPassword(password) Indstiller adgangskoden til password , som er en streng, der repræsenterer en gyldig adgangskode

Bemærk, at adgangskoden du sender som argument til .setPassword() gemmes i almindelig tekst og kan hentes senere ved at kalde .password() . Dette er en alvorlig sikkerhedsrisiko som du bør undgå at indføre i din database applikationer. Du lærer en mere sikker tilgang i afsnittet Åbning af en databaseforbindelse senere i denne øvelse.

At oprette en forbindelse til en SQLite-database ved hjælp af QSqlDatabase , åbn en interaktiv Python-session og indtast følgende kode:

>>>
>>> from PyQt5.QtSql import QSqlDatabase

>>> con = QSqlDatabase.addDatabase("QSQLITE")
>>> con.setDatabaseName("contacts.sqlite")

>>> con
<PyQt5.QtSql.QSqlDatabase object at 0x7f0facec0c10>

>>> con.databaseName()
'contacts.sqlite'

>>> con.connectionName()
'qt_sql_default_connection'

Denne kode vil oprette et databaseforbindelsesobjekt ved hjælp af "QSQLITE" som forbindelsens driver og "contacts.sqlite" som forbindelsens databasenavn. Da du ikke videregiver et forbindelsesnavn til .addDatabase() , bliver den nyoprettede din standardforbindelse, hvis navn er "qt_sql_default_connection" .

I tilfælde af SQLite-databaser er databasenavnet normalt et filnavn eller en sti, der inkluderer databasens filnavn. Du kan også bruge det specielle navn ":memory:" for en database i hukommelsen.



Håndtering af flere forbindelser

Der kan være situationer, hvor du skal bruge flere forbindelser til en enkelt database. For eksempel vil du måske logge brugernes interaktioner med databasen ved hjælp af en specifik forbindelse for hver bruger.

I andre situationer skal du muligvis forbinde din applikation til flere databaser. For eksempel vil du måske oprette forbindelse til flere fjerndatabaser for at indsamle data for at udfylde eller opdatere en lokal database.

For at håndtere disse situationer kan du angive specifikke navne til dine forskellige forbindelser og henvise til hver forbindelse ved dens navn. Hvis du vil give din databaseforbindelse et navn, så send det navn som det andet argument til .addDatabase() :

>>>
>>> from PyQt5.QtSql import QSqlDatabase

>>> # First connection
>>> con1 = QSqlDatabase.addDatabase("QSQLITE", "con1")
>>> con1.setDatabaseName("contacts.sqlite")

>>> # Second connection
>>> con2 = QSqlDatabase.addDatabase("QSQLITE", "con2")
>>> con2.setDatabaseName("contacts.sqlite")

>>> con1
<PyQt5.QtSql.QSqlDatabase object at 0x7f367f5fbf90>
>>> con2
<PyQt5.QtSql.QSqlDatabase object at 0x7f3686dd7510>

>>> con1.databaseName()
'contacts.sqlite'
>>> con2.databaseName()
'contacts.sqlite'

>>> con1.connectionName()
'con1'
>>> con2.connectionName()
'con2'

Her opretter du to forskellige forbindelser til den samme database, contacts.sqlite . Hver forbindelse har sit eget forbindelsesnavn. Du kan bruge forbindelsesnavnet til at få en reference til en specifik forbindelse til enhver tid senere i din kode i henhold til dine behov. For at gøre dette kan du kalde .database() med et forbindelsesnavn:

>>>
>>> from PyQt5.QtSql import QSqlDatabase

>>> db = QSqlDatabase.database("con1", open=False)

>>> db.databaseName()
'contacts.sqlite'
>>> db.connectionName()
'con1'

I dette eksempel ser du at .database() tager to argumenter:

  1. connectionName indeholder det forbindelsesnavn, du skal bruge. Hvis du ikke videregiver et forbindelsesnavn, vil standardforbindelsen blive brugt.
  2. open indeholder en boolsk værdi, der fortæller .database() om du vil åbne forbindelsen automatisk eller ej. Hvis open er True (standard), og forbindelsen ikke er åben, åbnes forbindelsen automatisk.

Returværdien af ​​.database() er en reference til forbindelsesobjektet kaldet connectionName . Du kan bruge forskellige forbindelsesnavne til at få referencer til specifikke forbindelsesobjekter og derefter bruge dem til at administrere din database.



Brug af forskellige SQL-dykkere

Indtil videre har du lært, hvordan du opretter en databaseforbindelse ved hjælp af SQLite-driveren . Dette er ikke den eneste driver tilgængelig i PyQt. Biblioteket tilbyder et rigt sæt SQL-drivere, der giver dig mulighed for at bruge forskellige typer databasestyringssystemer i henhold til dine specifikke behov:

Chaufførnavn Databasestyringssystem
QDB2 IBM Db2 (version 7.1 og nyere)
QIBASE Borland InterBase
QMYSQL/MARIADB MySQL eller MariaDB (version 5.0 og nyere)
QOCI Oracle Call Interface
QODBC Open Database Connectivity (ODBC)
QPSQL PostgreSQL (version 7.3 og nyere)
QSQLITE2 SQLite 2 (forældet siden Qt 5.14)
QSQLITE SQLite 3
QTDS Sybase Adaptive Server (forældet siden Qt 4.7)

Kolonnen Drivernavn indeholder identifikatorstrengene som du skal videregive til .addDatabase() som sit første argument for at bruge den tilknyttede driver. I modsætning til SQLite-driveren, når du bruger en anden driver, skal du muligvis indstille flere attributter, såsom databaseName , hostName , username , og password , for at forbindelsen fungerer korrekt.

Databasedrivere er afledt af QSqlDriver . Du kan oprette dine egne databasedrivere ved at underklassificere QSqlDriver , men det emne går ud over dette selvstudiums omfang. Hvis du er interesseret i at oprette dine egne databasedrivere, så tjek Sådan skriver du din egen databasedriver for flere detaljer.



Åbning af en databaseforbindelse

Når du har en databaseforbindelse, skal du åbne denne forbindelse for at kunne interagere med din database. For at gøre det, ringer du til .open() på forbindelsesobjektet. .open() har følgende to variationer:

  1. .open() åbner en databaseforbindelse ved hjælp af de aktuelle forbindelsesværdier.
  2. .open(username, password) åbner en databaseforbindelse ved hjælp af det angivne username og password .

Begge varianter returnerer True hvis forbindelsen lykkes. Ellers returnerer de False . Hvis forbindelsen ikke kan etableres, kan du kalde .lastError() for at få information om, hvad der skete. Denne funktion returnerer information om den sidste fejl rapporteret af databasen.

Bemærk: Som du har lært før, .setPassword(password) gemmer adgangskoder som almindelig tekst, hvilket er en sikkerhedsrisiko. På den anden side, .open() gemmer slet ikke adgangskoder. Den videregiver adgangskoden direkte til driveren, når forbindelsen åbnes. Derefter kasserer den adgangskoden. Så ved at bruge .open() at administrere dine adgangskoder er vejen at gå, hvis du vil forhindre sikkerhedsproblemer.

Her er et eksempel på, hvordan man åbner en SQLite-databaseforbindelse ved hjælp af den første variation af .open() :

>>>
>>> from PyQt5.QtSql import QSqlDatabase

>>> # Create the connection
>>> con = QSqlDatabase.addDatabase("QSQLITE")
>>> con.setDatabaseName("contacts.sqlite")

>>> # Open the connection
>>> con.open()
True
>>> con.isOpen()
True

I ovenstående eksempel opretter du først en forbindelse til din SQLite-database og åbner den forbindelse ved hjælp af .open() . Siden .open() returnerer True , forbindelsen er vellykket. På dette tidspunkt kan du kontrollere forbindelsen ved hjælp af .isOpen() , som returnerer True hvis forbindelsen er åben og False ellers.

Bemærk: Hvis du kalder .open() på en forbindelse, der bruger SQLite-driveren, og databasefilen ikke eksisterer, vil der automatisk blive oprettet en ny og tom databasefil.

I applikationer fra den virkelige verden skal du sikre dig, at du har en gyldig forbindelse til din database, før du forsøger at udføre nogen handlinger på dine data. Ellers kan din applikation gå i stykker og mislykkes. For eksempel, hvad hvis du ikke har skrivetilladelser til den mappe, hvor du forsøger at oprette den databasefil? Du skal sikre dig, at du håndterer enhver fejl, der kan opstå, mens du åbner en forbindelse.

En almindelig måde at kalde .open() er at pakke det ind i en betinget erklæring. Dette giver dig mulighed for at håndtere fejl, der kan opstå, når du åbner forbindelsen:

>>>
>>> import sys
>>> from PyQt5.QtSql import QSqlDatabase

>>> # Create the connection
>>> con = QSqlDatabase.addDatabase("QSQLITE")
>>> con.setDatabaseName("contacts.sqlite")

>>> # Open the connection and handle errors
>>> if not con.open():
...     print("Unable to connect to the database")
...     sys.exit(1)

Ombryder opkaldet til .open() i en betinget erklæring giver dig mulighed for at håndtere enhver fejl, der opstår, når du åbner forbindelsen. På denne måde kan du informere dine brugere om eventuelle problemer, før applikationen kører. Bemærk, at applikationen afsluttes med en afslutningsstatus på 1 , som sædvanligvis bruges til at angive en programfejl.

I ovenstående eksempel bruger du .open() i en interaktiv session, så du bruger print() at præsentere fejlmeddelelser til brugerne. Men i GUI-applikationer, i stedet for at bruge print() , bruger du normalt en QMessageBox objekt. Med QMessageBox , kan du oprette små dialogbokse for at præsentere oplysninger for dine brugere.

Her er et eksempel på en GUI-applikation, der illustrerer en måde at håndtere forbindelsesfejl på:

 1import sys
 2
 3from PyQt5.QtSql import QSqlDatabase
 4from PyQt5.QtWidgets import QApplication, QMessageBox, QLabel
 5
 6# Create the connection
 7con = QSqlDatabase.addDatabase("QSQLITE")
 8con.setDatabaseName("/home/contacts.sqlite")
 9
10# Create the application
11app = QApplication(sys.argv)
12
13# Try to open the connection and handle possible errors
14if not con.open():
15    QMessageBox.critical(
16        None,
17        "App Name - Error!",
18        "Database Error: %s" % con.lastError().databaseText(),
19    )
20    sys.exit(1)
21
22# Create the application's window
23win = QLabel("Connection Successfully Opened!")
24win.setWindowTitle("App Name")
25win.resize(200, 100)
26win.show()
27sys.exit(app.exec_())

if erklæring i linje 14 kontrollerer, om forbindelsen var mislykket. Hvis /home/ biblioteket eksisterer ikke, eller hvis du ikke har tilladelse til at skrive i det, kaldes det til .open() mislykkes, fordi databasefilen ikke kan oprettes. I denne situation er udførelsesstrømmen indtaster if erklæringskodeblok og viser en meddelelse på skærmen.

Hvis du ændrer stien til en anden mappe, som du kan skrive i, kaldes .open() vil lykkes, og du vil se et vindue, der viser meddelelsen Connection Successfully Opened! Du vil også have en ny databasefil kaldet contacts.sqlite i den valgte mappe.

Bemærk, at du sender None som beskedens forælder fordi du på tidspunktet for visning af beskeden ikke har oprettet et vindue endnu, så du har ikke en levedygtig forælder til beskedboksen.




Kørsel af SQL-forespørgsler med PyQt

Med en fuldt funktionel databaseforbindelse er du klar til at begynde at arbejde med din database. For at gøre det kan du bruge strengbaserede SQL-forespørgsler og QSqlQuery genstande. QSqlQuery giver dig mulighed for at køre enhver form for SQL-forespørgsel i din database. Med QSqlQuery , kan du udføre datamanipulationssprog (DML)-sætninger, såsom SELECT , INSERT , UPDATE og DELETE , samt datadefinitionssprog (DDL)-sætninger, såsom CREATE TABLE og så videre.

Konstruktøren af ​​QSqlQuery har flere variationer, men i denne tutorial vil du lære om to af dem:

  1. QSqlQuery(query, connection) konstruerer et forespørgselsobjekt ved hjælp af en streng-baseret SQL query og en database connection . Hvis du ikke angiver en forbindelse, eller hvis den angivne forbindelse er ugyldig, bruges standarddatabaseforbindelsen. Hvis query ikke er en tom streng, så udføres den med det samme.

  2. QSqlQuery(connection) konstruerer et forespørgselsobjekt ved hjælp af connection . Hvis connection er ugyldig, så bruges standardforbindelsen.

Du kan også oprette QSqlQuery objekter uden at videregive nogen argumenter til konstruktøren. I så fald vil forespørgslen bruge standarddatabaseforbindelsen, hvis nogen.

For at udføre en forespørgsel skal du kalde .exec() på forespørgselsobjektet. Du kan bruge .exec() på to forskellige måder:

  1. .exec(query) udfører den strengbaserede SQL-forespørgsel indeholdt i query . Det returnerer True hvis forespørgslen lykkedes og ellers returnerer False .

  2. .exec() udfører en tidligere forberedt SQL-forespørgsel. Det returnerer True hvis forespørgslen lykkedes og ellers returnerer False .

Bemærk: PyQt implementerer også variationer af QSqlQuery.exec() med navnet .exec_() . Disse giver bagudkompatibilitet med ældre versioner af Python, hvor exec var et nøgleord for sproget.

Nu hvor du kender det grundlæggende i at bruge QSqlQuery for at oprette og udføre SQL-forespørgsler, er du klar til at lære, hvordan du omsætter din viden i praksis.


Udførelse af statiske SQL-forespørgsler

For at begynde at oprette og udføre forespørgsler med PyQt, skal du starte din foretrukne kodeeditor eller IDE og oprette et Python-script kaldet queries.py . Gem scriptet og tilføj følgende kode til det:

 1import sys
 2
 3from PyQt5.QtSql import QSqlDatabase, QSqlQuery
 4
 5# Create the connection
 6con = QSqlDatabase.addDatabase("QSQLITE")
 7con.setDatabaseName("contacts.sqlite")
 8
 9# Open the connection
10if not con.open():
11    print("Database Error: %s" % con.lastError().databaseText())
12    sys.exit(1)
13
14# Create a query and execute it right away using .exec()
15createTableQuery = QSqlQuery()
16createTableQuery.exec(
17    """
18    CREATE TABLE contacts (
19        id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
20        name VARCHAR(40) NOT NULL,
21        job VARCHAR(50),
22        email VARCHAR(40) NOT NULL
23    )
24    """
25)
26
27print(con.tables())

I dette script starter du med at importere de moduler og klasser, som du skal arbejde med. Derefter opretter du en databaseforbindelse ved hjælp af .addDatabase() med SQLite-driveren. Du indstiller databasenavnet til "contacts.sqlite" og åbn forbindelsen.

For at oprette din første forespørgsel, instansierer du QSqlQuery uden nogen argumenter. Med forespørgselsobjektet på plads kalder du .exec() , der sender en streng-baseret SQL-forespørgsel som et argument. Denne form for forespørgsel er kendt som en statisk forespørgsel fordi den ikke får nogen parametre uden for forespørgslen.

Ovenstående SQL-forespørgsel opretter en ny tabel kaldet contacts i din database. Denne tabel vil have følgende fire kolonner:

Kolonne Indhold
id Et heltal med tabellens primære nøgle
name En streng med navnet på en kontakt
job En streng med jobtitlen på en kontakt
email En streng med en kontakts e-mail

Den sidste linje i ovenstående script udskriver listen over tabeller indeholdt i din database. Hvis du kører scriptet, vil du bemærke, at en ny databasefil kaldet contacts.sqlite oprettes i dit nuværende bibliotek. Du får også noget som ['contacts', 'sqlite_sequence'] udskrevet på din skærm. Denne liste indeholder navnene på tabellerne i din database.

Bemærk: En strengbaseret SQL-forespørgsel skal bruge en passende syntaks i henhold til den specifikke SQL-database, som du forespørger på. Hvis syntaksen er forkert, så .exec() ignorerer forespørgslen og returnerer False .

I tilfælde af SQLite kan forespørgslen kun indeholde én sætning ad gangen.

Kalder .exec() på en QSqlQuery objekt er en almindelig måde at udføre streng-baserede SQL-forespørgsler på dine databaser på, men hvad nu hvis du vil forberede dine forespørgsler på forhånd til senere udførelse? Det er emnet for næste afsnit.



Udførelse af dynamiske forespørgsler:Strengformatering

Indtil videre har du lært, hvordan du udfører statiske forespørgsler på en database. Statiske forespørgsler er dem, der ikke accepterer parametre , så forespørgslen kører, som den er. Selvom disse forespørgsler er ret nyttige, skal du nogle gange oprette forespørgsler, der henter data som svar på bestemte inputparametre.

Forespørgsler, der accepterer parametre på udførelsestidspunktet, er kendt som dynamiske forespørgsler . Brug af parametre giver dig mulighed for at finjustere forespørgslen og hente data som svar på specifikke parameterværdier. Forskellige værdier vil give forskellige resultater. Du kan tage inputparametre i en forespørgsel ved at bruge en af ​​følgende to tilgange:

  1. Byg forespørgslen dynamisk ved at bruge strengformatering til at interpolere parameterværdier.
  2. Forbered forespørgslen ved hjælp af pladsholderparametre, og bind derefter specifikke værdier til parametre.

Den første tilgang giver dig mulighed for hurtigt at oprette dynamiske forespørgsler. Men for at bruge denne tilgang sikkert, skal du være sikker på, at dine parameterværdier kommer fra en pålidelig kilde. Ellers kan du blive udsat for SQL-injektionsangreb.

Her er et eksempel på, hvordan man bruger strengformatering til at oprette dynamiske forespørgsler i PyQt:

>>>
>>> from PyQt5.QtSql import QSqlQuery, QSqlDatabase

>>> con = QSqlDatabase.addDatabase("QSQLITE")
>>> con.setDatabaseName("contacts.sqlite")
>>> con.open()
True

>>> name = "Linda"
>>> job = "Technical Lead"
>>> email = "[email protected]"

>>> query = QSqlQuery()
>>> query.exec(
...     f"""INSERT INTO contacts (name, job, email)
...     VALUES ('{name}', '{job}', '{email}')"""
... )
True

I dette eksempel bruger du en f-streng til at oprette en dynamisk forespørgsel ved at interpolere specifikke værdier i en streng-baseret SQL-forespørgsel. Den sidste forespørgsel indsætter data i dine contacts tabel, som nu indeholder data om Linda .

Bemærk: Senere i denne øvelse vil du se, hvordan du henter og navigerer i de data, der er gemt i en database.

Bemærk, at for at denne form for dynamisk forespørgsel skal fungere, skal du sikre dig, at de værdier, der skal indsættes, har den rigtige datatype. Så du bruger enkelte anførselstegn omkring pladsholderen i f-strengen, fordi disse værdier skal være strenge.



Udførelse af dynamiske forespørgsler:Pladsholderparametre

Den anden tilgang til udførelse af dynamiske forespørgsler kræver, at du forbereder dine forespørgsler på forhånd ved hjælp af en skabelon med pladsholdere for parametre. PyQt understøtter to parameterpladsholderstile:

  1. Oracle-stil bruger navngivne pladsholdere såsom :name eller :email .
  2. ODBC-stil bruger et spørgsmålstegn (? ) som en positionel pladsholder.

Bemærk, at disse stilarter ikke kan blandes i den samme forespørgsel. Du kan tjekke tilgange til bindende værdier for ekstra eksempler på, hvordan du bruger pladsholdere.

Bemærk: ODBC står for Open Database Connectivity.

For at oprette denne form for dynamisk forespørgsel i PyQt skal du først oprette en skabelon med en pladsholder for hver forespørgselsparameter og derefter sende den skabelon som et argument til .prepare() , som analyserer, kompilerer og forbereder forespørgselsskabelonen til udførelse. Hvis skabelonen har problemer, såsom en SQL-syntaksfejl, skal du .prepare() undlader at kompilere skabelonen og returnerer False .

Hvis forberedelsesprocessen lykkes, så prepare() returnerer True . Derefter kan du sende en specifik værdi til hver parameter ved hjælp af .bindValue() med navngivne eller positionelle parametre eller ved at bruge .addBindValue() med positionsparametre. .bindValue() har følgende to variationer:

  1. .bindValue(placeholder, val)
  2. .bindValue(pos, val)

I den første variant, placeholder repræsenterer en pladsholder i Oracle-stil. I den anden variant, pos repræsenterer et nul-baseret heltal med positionen af ​​en parameter i forespørgslen. I begge varianter, val indeholder den værdi, der skal bindes til en bestemt parameter.

.addBindValue() tilføjer en værdi til listen over pladsholdere ved hjælp af positionsbinding. Det betyder, at rækkefølgen af ​​kaldene til .addBindValue() bestemmer, hvilken værdi der vil være bundet til hver pladsholderparameter i den forberedte forespørgsel.

For at begynde at bruge forberedte forespørgsler kan du forberede en INSERT INTO SQL-sætning til at udfylde din database med nogle eksempeldata. Gå tilbage til det script, du oprettede i afsnittet Udførelse af statiske SQL-forespørgsler, og tilføj følgende kode lige efter kaldet til print() :

28# Creating a query for later execution using .prepare()
29insertDataQuery = QSqlQuery()
30insertDataQuery.prepare(
31    """
32    INSERT INTO contacts (
33        name,
34        job,
35        email
36    )
37    VALUES (?, ?, ?)
38    """
39)
40
41# Sample data
42data = [
43    ("Joe", "Senior Web Developer", "[email protected]"),
44    ("Lara", "Project Manager", "[email protected]"),
45    ("David", "Data Analyst", "[email protected]"),
46    ("Jane", "Senior Python Developer", "[email protected]"),
47]
48
49# Use .addBindValue() to insert data
50for name, job, email in data:
51    insertDataQuery.addBindValue(name)
52    insertDataQuery.addBindValue(job)
53    insertDataQuery.addBindValue(email)
54    insertDataQuery.exec()

Det første trin er at oprette en QSqlQuery objekt. Så kalder du .prepare() på forespørgselsobjektet. I dette tilfælde bruger du ODBC-stilen til pladsholderne. Din forespørgsel vil tage værdier for din kontakts name , job og email , så du skal bruge tre pladsholdere. Siden id kolonne er et autoinkrementeret heltal, du behøver ikke at angive værdier for det.

Derefter opretter du nogle eksempeldata for at udfylde databasen. data indeholder en liste over tuples, og hver tuple indeholder tre elementer:navn, job og e-mail for hver kontakt.

Det sidste trin er at binde de værdier, du vil videregive til hver pladsholder og derefter kalde .exec() for at udføre forespørgslen. For at gøre det bruger du en for sløjfe. Sløjfehovedet pakker hver tuple ud i data i tre separate variabler med praktiske navne. Then you call .addBindValue() on the query object to bind the values to the placeholders.

Note that you’re using positional placeholders , so the order in which you call .addBindValue() will define the order in which each value is passed to the corresponding placeholder.

This approach for creating dynamic queries is handy when you want to customize your queries using values that come from your user’s input. Anytime you take the user’s input to complete a query on a database, you face the security risk of SQL injection.

In PyQt, combining .prepare() , .bindValue() , and .addBindValue() fully protects you against SQL injection attacks, so this is the way to go when you’re taking untrusted input to complete your queries.



Navigating the Records in a Query

If you execute a SELECT statement, then your QSqlQuery object will retrieve zero or more records from one or more tables in your database. The query will hold records containing data that matches the query’s criteria. If no data matches the criteria, then your query will be empty.

QSqlQuery provides a set of navigation methods that you can use to move throughout the records in a query result:

Method Retrieves
.next() The next record
.previous() The previous record
.first() The first record
.last() The last record
.seek(index, relative=False) The record at position index

All these methods position the query object on the retrieved record if that record is available. Most of these methods have specific rules that apply when using them. With these methods, you can move forward, backward, or arbitrarily through the records in a query result. Since they all return either True or False , you can use them in a while loop to navigate all the records in one go.

These methods work with active queries . A query is active when you’ve successfully run .exec() on it, but the query isn’t finished yet. Once an active query is on a valid record, you can retrieve data from that record using .value(index) . This method takes a zero-based integer number, index , and returns the value at that index (column) in the current record.

Bemærk: If you execute a SELECT * type of query, then the columns in the result won’t follow a known order. This might cause problems when you use .value() to retrieve the value at a given column because there’s no way of knowing if you’re using the right column index.

You’ll look at a few examples of how to use some of the navigation methods to move throughout a query below. But first, you need to create a connection to your database:

>>>
>>> from PyQt5.QtSql import QSqlDatabase, QSqlQuery

>>> con = QSqlDatabase.addDatabase("QSQLITE")
>>> con.setDatabaseName("contacts.sqlite")
>>> con.open()
True

Here, you create and open a new connection to contacts.sqlite . If you’ve been following along with this tutorial so far, then this database already contains some sample data. Now you can create a QSqlQuery object and execute it on that data:

>>>
>>> # Create and execute a query
>>> query = QSqlQuery()
>>> query.exec("SELECT name, job, email FROM contacts")
True

This query retrieves data about the name , job , and email of all the contacts stored in the contacts bord. Since .exec() returned True , the query was successful and is now an active query. You can navigate the records in this query using any of the navigation methods you saw before. You can also retrieve the data at any column in a record using .value() :

>>>
>>> # First record
>>> query.first()
True

>>> # Named indices for readability
>>> name, job, email = range(3)

>>> # Retrieve data from the first record
>>> query.value(name)
'Linda'

>>> # Next record
>>> query.next()
True
>>> query.value(job)
'Senior Web Developer'

>>> # Last record
>>> query.last()
True
>>> query.value(email)
'[email protected]'

With the navigation methods, you can move around the query result. With .value() , you can retrieve the data at any column in a given record.

You can also iterate through all the records in your query using a while loop along with .next() :

>>>
>>> query.exec()
True

>>> while query.next():
...     print(query.value(name), query.value(job), query.value(email))
...
Linda Technical Lead [email protected]
Joe Senior Web Developer [email protected]
...

With .next() , you navigate all the records in a query result. .next() works similar to the iterator protocol in Python. Once you’ve iterated over the records in a query result, .next() starts returning False until you run .exec() en gang til. A call to .exec() retrieves data from a database and places the query object’s internal pointer one position before the first record, so when you call .next() , you get the first record again.

You can also loop in reverse order using .previous() :

>>>
>>> while query.previous():
...     print(query.value(name), query.value(job), query.value(email))
...
Jane Senior Python Developer [email protected]
David Data Analyst [email protected]
...

.previous() works similar to .next() , but the iteration is done in reverse order. In other words, the loop goes from the query pointer’s position back to the first record.

Sometimes you might want to get the index that identifies a given column in a table by using the name of that column. To do that, you can call .indexOf() on the return value of .record() :

>>>
>>> query.first()
True

>>> # Get the index of name
>>> name = query.record().indexOf("name")

>>> query.value(name)
'Linda'

>>> # Finish the query object if unneeded
>>> query.finish()
>>> query.isActive()
False

The call to .indexOf() on the result of .record() returns the index of the "name" kolonne. If "name" doesn’t exist, then .indexOf() returnerer -1 . This is handy when you use a SELECT * statement in which the order of columns is unknown. Finally, if you’re done with a query object, then you can turn it inactive by calling .finish() . This will free the system memory associated with the query object at hand.




Closing and Removing Database Connections

In practice, some of your PyQt applications will depend on a database, and others won’t. An application that depends on a database often creates and opens a database connection just before creating any window or graphical component and keeps the connection open until the application is closed.

On the other hand, applications that don’t depend on a database but use a database to provide some of their functionalities typically connect to that database only when needed, if at all. In these cases, you can close the connection after use and free the resources associated with that connection, such as system memory.

To close a connection in PyQt, you call .close() on the connection. This method closes the connection and frees any acquired resources. It also invalidates any associated QSqlQuery objects because they can’t work properly without an active connection.

Here’s an example of how to close an active database connection using .close() :

>>>
>>> from PyQt5.QtSql import QSqlDatabase

>>> con = QSqlDatabase.addDatabase("QSQLITE")
>>> con.setDatabaseName("contacts.sqlite")

>>> con.open()
True
>>> con.isOpen()
True

>>> con.close()
>>> con.isOpen()
False

You can call .close() on a connection to close it and free all its associated resources. To make sure that a connection is closed, you call .isOpen() .

Note that QSqlQuery objects remain in memory after closing their associated connection, so you must make your queries inactive by calling .finish() or .clear() , or by deleting the QSqlQuery object before closing the connection. Otherwise, residual memory is left out in your query object.

You can reopen and reuse any previously closed connection. That’s because .close() doesn’t remove connections from the list of available connections, so they remain usable.

You can also completely remove your database connections using .removeDatabase() . To do this safely, first finish your queries using .finish() , then close the database using .close() , and finally remove the connection. You can use .removeDatabase(connectionName) to remove the database connection called connectionName from the list of available connections. Removed connections are no longer available for use in the application at hand.

To remove the default database connection, you can call .connectionName() on the object returned by .database() and pass the result to .removeDatabase() :

>>>
>>> # The connection is closed but still in the list of connections
>>> QSqlDatabase.connectionNames()
['qt_sql_default_connection']

>>> # Remove the default connection
>>> QSqlDatabase.removeDatabase(QSqlDatabase.database().connectionName())

>>> # The connection is no longer in the list of connections
>>> QSqlDatabase.connectionNames()
[]

>>> # Try to open a removed connection
>>> con.open()
False

Here, the call to .connectionNames() returns the list of available connections. In this case, you have only one connection, the default. Then you remove the connection using .removeDatabase() .

Bemærk: Before closing and removing a database connection, you need to make sure that everything that uses the connection is deleted or set to use a different data source. Otherwise, you can have a resource leak .

Since you need a connection name to use .removeDatabase() , you call .connectionName() on the result of .database() to get the name of the default connection. Finally, you call .connectionNames() again to make sure that the connection is no longer in the list of available connections. Trying to open a removed connection will return False because the connection no longer exists.



Displaying and Editing Data With PyQt

A common requirement in GUI applications that use databases is the ability to load, display, and edit data from the database using different widgets. Table, list, and tree widgets are commonly used in GUIs to manage data.

PyQt provides two different kind of widgets for managing data:

  1. Standard widgets include internal containers for storing data.
  2. View widgets don’t maintain internal data containers but use models to access data.

For small GUI applications that manage small databases, you can use the first approach. The second approach is handy when you’re building complex GUI applications that manage large databases.

The second approach takes advantage of PyQt’s Model-View programming. With this approach, you have widgets that represent views such as tables, lists, and trees on one hand and model classes that communicate with your data on the other hand.


Understanding PyQt’s Model-View Architecture

The Model-View-Controller (MVC) design pattern is a general software pattern intended to divide an application’s code into three general layers, each with a different role.

The model takes care of the business logic of the application, the view provides on-screen representations, and the controller connects the model and the view to make the application work.

Qt provides a custom variation of MVC. They call it the Model-View architecture, and it’s available for PyQt as well. The pattern also separates the logic into three components:

  1. Models communicate with and access the data. They also define an interface that’s used by views and delegates to access the data. All models are based on QAbstractItemModel . Some commonly used models include QStandardItemModel , QFileSystemModel , and SQL-related models.

  2. Views are responsible for displaying the data to the user. They also have similar functionality to the controller in the MVC pattern. All views are based on QAbstractItemView . Some commonly used views are QListView , QTableView , and QTreeView .

  3. Delegates paint view items and provide editor widgets for modifying items. They also communicate back with the model if an item has been modified. The base class is QAbstractItemDelegate .

Separating classes into these three components implies that changes on models will be reflected on associated views or widgets automatically, and changes on views or widgets through delegates will update the underlying model automatically.

In addition, you can display the same data in different views without the need for multiple models.



Using Standard Widget Classes

PyQt provides a bunch of standard widgets for displaying and editing data in your GUI applications. These standard widgets provide views such as tables, trees, and lists. They also provide an internal container for storing data and convenient delegates for editing the data. All these features are grouped into a single class.

Here are three of these standard classes:

Standard Class Displays
QListWidget A list of items
QTreeWidget A hierarchical tree of items
QTableWidget A table of items

QTableWidget is arguably the most popular widget when it comes to displaying and editing data. It creates a 2D array of QTableWidgetItem objects. Each item holds an individual value as a string. All these values are displayed and organized in a table of rows and columns.

You can perform at least the following operations on a QTableWidget object:

  • Editing the content of its items using delegate objects
  • Adding new items using .setItem()
  • Setting the number of rows and columns using .setRowCount() and .setColumnCount()
  • Adding vertical and horizontal header labels using setHorizontalHeaderLabels() and .setVerticalHeaderLabels

Here’s a sample application that shows how to use a QTableWidget object to display data in a GUI. The application uses the database you created and populated in previous sections, so if you want to run it, then you need to save the code into the same directory in which you have the contacts.sqlite database:

If you double-click any cell of the table, then you’ll be able to edit the content of the cell. However, your changes won’t be saved to your database.

Here’s the code for your application:

 1import sys
 2
 3from PyQt5.QtSql import QSqlDatabase, QSqlQuery
 4from PyQt5.QtWidgets import (
 5    QApplication,
 6    QMainWindow,
 7    QMessageBox,
 8    QTableWidget,
 9    QTableWidgetItem,
10)
11
12class Contacts(QMainWindow):
13    def __init__(self, parent=None):
14        super().__init__(parent)
15        self.setWindowTitle("QTableView Example")
16        self.resize(450, 250)
17        # Set up the view and load the data
18        self.view = QTableWidget()
19        self.view.setColumnCount(4)
20        self.view.setHorizontalHeaderLabels(["ID", "Name", "Job", "Email"])
21        query = QSqlQuery("SELECT id, name, job, email FROM contacts")
22        while query.next():
23            rows = self.view.rowCount()
24            self.view.setRowCount(rows + 1)
25            self.view.setItem(rows, 0, QTableWidgetItem(str(query.value(0))))
26            self.view.setItem(rows, 1, QTableWidgetItem(query.value(1)))
27            self.view.setItem(rows, 2, QTableWidgetItem(query.value(2)))
28            self.view.setItem(rows, 3, QTableWidgetItem(query.value(3)))
29        self.view.resizeColumnsToContents()
30        self.setCentralWidget(self.view)
31
32def createConnection():
33    con = QSqlDatabase.addDatabase("QSQLITE")
34    con.setDatabaseName("contacts.sqlite")
35    if not con.open():
36        QMessageBox.critical(
37            None,
38            "QTableView Example - Error!",
39            "Database Error: %s" % con.lastError().databaseText(),
40        )
41        return False
42    return True
43
44app = QApplication(sys.argv)
45if not createConnection():
46    sys.exit(1)
47win = Contacts()
48win.show()
49sys.exit(app.exec_())

Here’s what’s happening in this example:

  • Lines 18 to 20 create a QTableWidget object, set the number of columns to 4 , and set user-friendly labels for each column’s header.
  • Line 21 creates and executes a SELECT SQL query on your database to get all the data in the contacts table.
  • Line 22 starts a while loop to navigate the records in the query result using .next() .
  • Line 24 increments the number of rows in the table by 1 using .setRowCount() .
  • Lines 25 to 28 add items of data to your table using .setItem() . Note that since the values in the id columns are integer numbers, you need to convert them into strings to be able to store them in a QTableWidgetItem objekt.

.setItem() takes three arguments:

  1. row holds a zero-based integer that represents the index of a given row in the table.
  2. column holds a zero-based integer that represents the index of a given column in the table.
  3. item holds the QTableWidgetItem object that you need to place at a given cell in the table.

Finally, you call .resizeColumnsToContents() on your view to adjust the size of the columns to their content and provide a better rendering of the data.

Displaying and editing database tables using standard widgets can become a challenging task. That’s because you’ll have two copies of the same data. In other words you’ll have a copy of the data in two locations:

  1. Outside the widget, in your database
  2. Inside the widget, in the widget’s internal containers

You’re responsible for synchronizing both copies of your data manually, which can be an annoying and error-prone operation. Luckily, you can use PyQt’s Model-View architecture to avoid most of these problems, as you’ll see in the following section.



Using View and Model Classes

PyQt’s Model-View classes eliminate the problems of data duplication and synchronization that may occur when you use standard widget classes to build database applications. The Model-View architecture allows you to use several views to display the same data because you can pass one model to many views.

Model classes provide an application programming interface (API) that you can use to manipulate data. View classes provide convenient delegate objects that you can use to edit data in the view directly. To connect a view with a given module, you need to call .setModel() on the view object.

PyQt offers a set of view classes that support the Model-View architecture:

View Class Displays
QListView A list of items that take values directly from a model class
QTreeView A hierarchical tree of items that take values directly from a model class
QTableView A table of items that take values directly from a model class

You can use these view classes along with model classes to create your database applications. This will make your applications more robust, faster to code, and less error-prone.

Here are some of the model classes that PyQt provides for working with SQL databases:

Model Class Beskrivelse
QSqlQueryModel A read-only data model for SQL queries
QSqlTableModel An editable data model for reading and writing records in a single table
QSqlRelationalTableModel An editable data model for reading and writing records in a relational table

Once you’ve connected one of these models to a physical database table or query, you can use them to populate your views. Views provide delegate objects that allow you to modify the data directly in the view. The model connected to the view will update the data in your database to reflect any change in the view. Note that you don’t have to update the data in the database manually. The model will do that for you.

Here’s an example that shows the basics of how to use a QTableView object and a QSqlTableModel object together to build a database application using PyQt’s Model-View architecture:

To edit the data in a cell of the table, you can double-click the cell. A convenient delegate widget will show in the cell, allowing you to edit its content. Then you can hit Enter to save the changes.

The ability to automatically handle and save changes in the data is one of the more important advantages of using PyQt’s Model-View classes. The Model-View architecture will improve your productivity and reduce the errors that can appear when you have to write data manipulation code by yourself.

Here’s the code to create the application:

 1import sys
 2
 3from PyQt5.QtCore import Qt
 4from PyQt5.QtSql import QSqlDatabase, QSqlTableModel
 5from PyQt5.QtWidgets import (
 6    QApplication,
 7    QMainWindow,
 8    QMessageBox,
 9    QTableView,
10)
11
12class Contacts(QMainWindow):
13    def __init__(self, parent=None):
14        super().__init__(parent)
15        self.setWindowTitle("QTableView Example")
16        self.resize(415, 200)
17        # Set up the model
18        self.model = QSqlTableModel(self)
19        self.model.setTable("contacts")
20        self.model.setEditStrategy(QSqlTableModel.OnFieldChange)
21        self.model.setHeaderData(0, Qt.Horizontal, "ID")
22        self.model.setHeaderData(1, Qt.Horizontal, "Name")
23        self.model.setHeaderData(2, Qt.Horizontal, "Job")
24        self.model.setHeaderData(3, Qt.Horizontal, "Email")
25        self.model.select()
26        # Set up the view
27        self.view = QTableView()
28        self.view.setModel(self.model)
29        self.view.resizeColumnsToContents()
30        self.setCentralWidget(self.view)
31
32def createConnection():
33    con = QSqlDatabase.addDatabase("QSQLITE")
34    con.setDatabaseName("contacts.sqlite")
35    if not con.open():
36        QMessageBox.critical(
37            None,
38            "QTableView Example - Error!",
39            "Database Error: %s" % con.lastError().databaseText(),
40        )
41        return False
42    return True
43
44app = QApplication(sys.argv)
45if not createConnection():
46    sys.exit(1)
47win = Contacts()
48win.show()
49sys.exit(app.exec_())

Here’s what’s happening in this code:

  • Line 18 creates an editable QSqlTableModel objekt.
  • Line 19 connects your model with the contacts table in your database using .setTable() .
  • Line 20 sets the edit strategy of the model to OnFieldChange . This strategy allows the model to automatically update the data in your database if the user modifies any of the data directly in the view.
  • Lines 21 to 24 set some user-friendly labels to the horizontal headers of the model using .setHeaderData() .
  • Line 25 loads the data from your database and populates the model by calling .select() .
  • Line 27 creates the table view object to display the data contained in the model.
  • Line 28 connects the view with the model by calling .setModel() on the view with your data model as an argument.
  • Line 29 calls .resizeColumnsToContents() on the view object to adjust the table to its content.

Det er det! You now have a fully-functional database application.




Using SQL Databases in PyQt:Best Practices

When it comes to using PyQt’s SQL support effectively, there are some best practices that you might want to use in your applications:

  • Favor PyQt’s SQL support over Python standard library or third-party libraries to take advantage of the natural integration of these classes with the rest of PyQt’s classes and infrastructure, mostly with the Model-View architecture.

  • Use previously prepared dynamic queries with placeholders for parameters and bind values to those parameters using .addBindValue() and .bindValue() . This will help prevent SQL injection attacks.

  • Handle errors that can occur when opening a database connection to avoid unexpected behaviors and application crashes.

  • Close and remove unneeded database connections and queries to free any acquired system resources.

  • Minimize the use of SELECT * queries to avoid problems when retrieving data with .value() .

  • Pass your passwords to .open() instead of to .setPassword() to avoid the risk of compromising your security.

  • Take advantage of PyQt’s Model-View architecture and its integration with PyQt’s SQL support to make your applications more robust.

This list isn’t complete, but it’ll help you make better use of PyQt’s SQL support when developing your database applications.



Konklusion

Using PyQt’s built-in support to work with SQL databases is an important skill for any Python developer who’s creating PyQt GUI applications and needs to connect them to a database. PyQt provides a consistent set of classes for managing SQL databases.

These classes fully integrate with PyQt’s Model-View architecture, allowing you to develop GUI applications that can manage databases in a user-friendly way.

In this tutorial, you’ve learned how to:

  • Use PyQt’s SQL support to connect to a database
  • Execute SQL queries on a database with PyQt
  • Build database applications using PyQt’s Model-View architecture
  • Display and edit data from a database using PyQt widgets

With this knowledge, you can improve your productivity when creating nontrivial database applications and make your GUI applications more robust.



  1. SQL Server:Indekskolonner brugt i lignende?

  2. Optagelsesbaseret indsættelse og opdatering i Oracle

  3. Pivottabel med ikke-kardinalværdier

  4. ListView-kontrol med Ms-Access TreeView