This is a Python 3.9 script that pulls data from a MySQL database and converts it to a list of named tuples and then passing it to PyQt6.QTreeWidget
.
It has to be a list
of namedtuples
, because I am going to store lists of tags packed in strings dumped from json.dumps
in single fields, then using json.loads
to unpack the lists. This way, filter by tags can easily done with issubset
method.
This will be the main interface of a GUI music player project I am working on, though it is still bare bones, at least the tree hierarchy part is complete.
This is the glimpse of the database:
This is what the window looks like:
And here is the code:
import sys
import mysql.connector
from collections import namedtuple
from PyQt6.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem
song = namedtuple('song', 'artist title album')
app = QApplication(sys.argv)
conn = mysql.connector.connect(user='Estranger', password=********, host='127.0.0.1', port=3306, database='Music')
cursor = conn.cursor()
cursor.execute('select artist, title, album from songs')
songs = [song(*i) for i in cursor.fetchall()]
conn.commit()
tree = QTreeWidget()
tree.resize(1280,720)
tree.setWindowTitle('tree')
frame = tree.frameGeometry()
center = tree.screen().availableGeometry().center()
frame.moveCenter(center)
tree.move(frame.topLeft())
tree.setColumnCount(4)
tree.setHeaderLabels(['Name', 'Artist', 'Album', 'Title'])
entries = []
for p in sorted(set([_.artist for _ in songs])):
artist = QTreeWidgetItem([p])
for a in sorted(set([_.album for _ in songs if _.artist == p])):
album = QTreeWidgetItem([a, p, a])
for s in [_.title for _ in songs if _.artist == p and _.album == a]:
song = QTreeWidgetItem([s, p, a, s])
album.addChild(song)
artist.addChild(album)
entries.append(artist)
tree.insertTopLevelItems(0, entries)
tree.show()
sys.exit(app.exec())
(Password obscured for obvious reasons)
I adapted my code from here: https://doc.qt.io/qtforpython-6/tutorials/basictutorial/treewidget.html
The code there:
import sys
from PySide6.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem
data = {"Project A": ["file_a.py", "file_a.txt", "something.xls"],
"Project B": ["file_b.csv", "photo.jpg"],
"Project C": []}
app = QApplication()
tree = QTreeWidget()
tree.setColumnCount(2)
tree.setHeaderLabels(["Name", "Type"])
items = []
for key, values in data.items():
item = QTreeWidgetItem([key])
for value in values:
ext = value.split(".")[-1].upper()
child = QTreeWidgetItem([value, ext])
item.addChild(child)
items.append(item)
tree.insertTopLevelItems(0, items)
tree.show()
sys.exit(app.exec())
In the example, the data is already hierarchical, it is a dictionary structured like a tree, but my data is a list of named tuples which is kind of flat and more suitable for a table.
But I prefer using a tree-like view to present the data.
Because my data is flat, and the example code structure needs the data to be hierarchical, I used list comprehensions plus cast to set
plus sorted
function to make the hierarchy on the fly, which generates a lot of loops overhead, which is silly.
I am really new to this, this is my first attempt at creating a GUI program, I have Google searched for days and haven't find a way to populate a QTreeView
or QTreeWidget
with data from a flat list, so I wrote my own.
My question is, what is a smarter, more efficient way to achieve the same result (populate a tree-like structure with data from a list of named tuples), and how can I use QTreeView
instead of QTreeWidget
(if there is any difference)?
Any help is appreciated.
1 Answer 1
- You have a pile of unstructured, global code; this needs to be organized into functions and maybe classes
- Close your cursor once you're done with it, ideally via context management
- Order your query using the database itself, to make grouping less painful; also query in order of hierarchy (artist, album, song)
- You don't need
entries
; just add items one at a time - 4 should not be hard-coded
- Your repeated set comprehensions can be greatly simplified (i.e. abolished) via
itertools.groupby
This example code localises the MySQL import because I don't have it; I've generated fake data instead.
import sys
from dataclasses import dataclass
from itertools import groupby
from typing import Iterable, Callable
from PyQt6.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem
@dataclass
class Song:
artist: str
album: str
title: str
@classmethod
def from_db(cls) -> Iterable['Song']:
from mysql.connector import connect
with connect(
user='Estranger',
password='********',
host='127.0.0.1',
port=3306,
database='Music',
) as conn, conn.cursor() as cursor:
cursor.execute(
'select artist, album, title from songs '
'order by artist, album'
)
for row in cursor.fetchall():
yield cls(*row)
@classmethod
def fake_rows(cls) -> Iterable['Song']:
return (
cls('Weird Al Yankovic', 'Running with Scissors', 'Pretty Fly for a Rabbi'),
cls('Weird Al Yankovic', 'Running with Scissors', 'Albuquerque'),
cls('Weird Al Yankovic', 'Off the Deep End', "I Can't Watch This"),
cls('Weird Al Yankovic', 'Off the Deep End', 'Smells like Nirvana'),
cls('The Arrogant Worms', "C'est Cheese", 'Mounted Animal Nature Trail'),
)
def by_artist(self): return self.artist
def by_album(self): return self.album
class GUI:
HEADERS = ('Name', 'Artist', 'Album', 'Title')
def __init__(self, songs):
app = QApplication(sys.argv)
tree = QTreeWidget()
# a reference needs to be held or this will be garbage-collected
self.tree = tree
tree.resize(1280, 720)
tree.setWindowTitle('tree')
frame = tree.frameGeometry()
center = tree.screen().availableGeometry().center()
frame.moveCenter(center)
tree.move(frame.topLeft())
tree.setColumnCount(len(self.HEADERS))
tree.setHeaderLabels(self.HEADERS)
for artist, after_artist in groupby(songs, Song.by_artist):
artist_node = QTreeWidgetItem((artist,))
for album, after_album in groupby(after_artist, Song.by_album):
album_node = QTreeWidgetItem((album, artist, album))
for song in after_album:
song_node = QTreeWidgetItem((song.title, artist, album, song.title))
album_node.addChild(song_node)
artist_node.addChild(album_node)
tree.addTopLevelItem(artist_node)
tree.show()
self.run: Callable[[], int] = app.exec
def main() -> None:
songs = Song.fake_rows()
gui = GUI(songs)
exit(gui.run())
if __name__ == '__main__':
main()
Explore related questions
See similar questions with these tags.