PDA

View Full Version : Model-/View-Programming: How to store the underlying data/different approach?



Jay-D
10th April 2013, 05:17
Hi,

I'm trying to make a small playlist for an audio player, that should support internal drag and drop to change the order of tracks and adding tracks via drag and drop from a folder. It should be like Winamp's playlist, just showing a few tracks in the order they will be played, not a huge list like in iTunes, displaying tons of information in a table. I'm using PyQt4 on Windows and at the moment, I'm only interested in Windows-compatibility.
So far, I've set up a QListView and a custom model deriving from QAbstractListModel, and my idea was to have a class "Song", to represent the individual tracks, that would provide a string for the data()-function in Qt.DisplayRole and store a few other things like duration and albumcover as member variables. By throwing together lines from numerous examples, I managed to get the internal drag and drop somewhat working, the only problem being that once an item has been moved, it's no longer of the type 'Song', but a QString instead. I also tried to get the “external” darg and drop working, by overwriting the dragEnterEvent of the QListView and filtering with "if event.mimeData().hasFormat('audio/mpeg'):", which of course broke the internal drag and drop.
Instead of thinking about also looking for my "Song" objects somehow, I'm now wondering if I should have a completely different approach. Or how and when I have to convert the items to "Song"-objects or something else. I find the documentation on Model-/View-Programming quite confusing and have the feeling the more I reread everything, the more confused I get. So I'm not even sure anymore, if I should use QListView or QListWidget instead. And I'm clueless how to store/pass the item data around. And if I'm supposed to provide indices myself (parent() and index() functions (and more?)) and how to do it. Are there any resources on the model/underlying data part of Model/View Programming, that could clarify things for me, preferably using PyQt?

Thanks in advance,
Jay-D

Santosh Reddy
10th April 2013, 09:32
So far, I've set up a QListView and a custom model deriving from QAbstractListModel, and my idea was to have a class "Song", to represent the individual tracks, that would provide a string for the data()-function in Qt.DisplayRole and store a few other things like duration and albumcover as member variables. By throwing together lines from numerous examples, I managed to get the internal drag and drop somewhat working, the only problem being that once an item has been moved, it's no longer of the type 'Song', but a QString instead. I also tried to get the “external” darg and drop working, by overwriting the dragEnterEvent of the QListView and filtering with "if event.mimeData().hasFormat('audio/mpeg'):", which of course broke the internal drag and drop.
Take a look at the mime data during en external drag n drop operation, when an internal drag n drop is started prepare the mime data just like the external mime data. This way you can handle internal or external drop in the same way.

d_stranz
10th April 2013, 22:18
it's no longer of the type 'Song', but a QString instead

You can always register your Song class with Qt's type system (see QMetaType) and define the streaming operators that would allow it to be copied into and out of a QVariant instance. Once you have done that, then you can transfer a Song instance as a Song type instead of a QString type. You would also define your own MIME type to allow you to identify your Song objects during drag and drop.

Jay-D
11th April 2013, 14:38
Thanks for your answers!

I tried to find out how to use QMimeData and noticed, that mp3-Files I drop don't have the mimeType 'audio/mpeg', that I would expect. Instead, when printing the contents of 'event.mimeData().formats()', I get:

application/x-qt-windows-mime;value="Shell IDList Array"
text/uri-list
application/x-qt-windows-mime;value="FileName"
application/x-qt-windows-mime;value="FileNameW"

Unfortunately, I'm completely new to the mime data subject, so I (once again) don't know what to do now...

d_stranz
11th April 2013, 17:33
mp3-Files I drop don't have the mimeType 'audio/mpeg'

From the list of mime types, it looks like Windoze is saying, "Launch the default shell application to try to open this mime type", which, depending on your associations, could be Windoze Media Player or some other third-party app.

I would guess that if you selected the "text/uri-list" ("uri" meaning "Universal Resource Identifier"), it would contain not the mp3 contents, but the address(es) of the selected file(s) on your PC as pseudo-URLs (e.g. file://c:/Music/myfile.mp3"). I am guessing that the QVariant would be a QStringList type.

If all of your drags and drops are internal to your app, I would think you could make up your own mime type string: "application/myapp-songs" and use that when putting your Song metatypes into the drag and drop mechanism. It wouldn't have any meaning outside your app (or another of your apps that could serve as a drop target), but if what you want is to move things around within your app, then it should be fine.

Jay-D
12th April 2013, 12:05
I would guess that if you selected the "text/uri-list" ("uri" meaning "Universal Resource Identifier"), it would contain not the mp3 contents, but the address(es) of the selected file(s) on your PC as pseudo-URLs (e.g. file://c:/Music/myfile.mp3"). I am guessing that the QVariant would be a QStringList type.
Exactly. As I mentioned before, I never heard about mime data before, so I wonder right now, if it makes sense using it here. The only point in using it I can see right now, is filtering for .mp3-files, but since my .mp3-files don't have the 'audio/mpeg' format, I'm thinking about accepting all drops, extracting the file uri(s) and checking if the path has the .mp3 suffix.
What I would like to do next, is create a new item, that displays a string "Artist - Title" and somehow stores the filepath and a few other values. I don't completely understand the code snippets, that I put together to create my own model (QAbstractListModel subclass), so I don't know, how to turn the filepath I retrieve from the drag-and-drop operation into an item in my list, that displays a string, stores a few values from the mp3's metadata, expose them to my app when doubleclicked and can be moved around inside my QListView.
Please bear with me, I'm having a hard time figuring out the whole view-model-item-etc. stuff. Any help or examples would be very appreciated.

d_stranz
12th April 2013, 18:59
I assume there's an API you can find somewhere that will let you extract mp3 metadata once you have an mp3 file in hand. How you put this into a model depends on how you wish to manipulate and display it.

Typically, the way I derive a custom model from QAbstractItemModel is to use it as a wrapper around the actual data stored in some vector, list, whatever externally to the model. So for example, your application might create and manipulate a QVector< Song * > collection, and keep this vector somewhere. When you create your QAbstractItemModel subclass, you add an extra method or two which lets you set a pointer to this vector into the model.

The model's data() method retrieves Song* instances from this vector (according to the row number), and depending on the column number (if it is a table), retrieves different attributes from the Song instance (like maybe column 0 is the title, column 1 is the artist, etc.). The other QAbstractItemModel attributes (rowCount, columnCount, etc.) also reference the external vector.

A good book for explaining in detail how the model-view architecture works is Advanced Qt Programming by Mark Summerfield. Among other things, it explains how to use the standard item model as well as how to create custom table and tree models.

By the way, you shouldn't accept all drops - this would give a misleading impression to the users of your program. Your program would indicate to them that it could drop the item, but then when it was dropped, nothing would happen. Better to examine the contents of the mime data in the dragEnterEvent() handler and decide whether or not to accept the event there.

Jay-D
13th April 2013, 03:03
Thanks, d_stranz.

The mp3 metadata part won't be a problem. I'm trying to store the data as a list of Song-instances, but I don't know where and how a dropped file/files should be inserted into the list. Should I insert new songs inside the dropEvent of the viewport or in some overridden method of the model? I can imagine it's not enough to just append new Song-instances, when the dropEvent happens, which functions should I reimplement?

By the way, you shouldn't accept all drops - this would give a misleading impression to the users of your program. Your program would indicate to them that it could drop the item, but then when it was dropped, nothing would happen. Better to examine the contents of the mime data in the dragEnterEvent() handler and decide whether or not to accept the event there.
That was my intention, should have stated that more clearly. I just meant it would be cleaner and more convenient if I could just look for the 'audio/mpeg' mimeData instead of checking for the file extension(s).
I'll see if I can get that book.

d_stranz
13th April 2013, 07:56
Should I insert new songs inside the dropEvent of the viewport or in some overridden method of the model?

Where you insert depends on where you are storing your song list. If the only place it is stored is inside the model, then as you decode the mp3 metadata on the drop, you create a Song instance and use a custom method to add it to the array inside the model. In this case, your custom method must do this:



void MyModel::addSongs( const QList< Song * > & addSongs )
{
- call beginInsertRows()
- append the new Song * instance(s) to the end of the QList< Song * >
- call endInsertRows()
}


Likewise, if you remove a song from the list:



void MyModel::removeSongs( const QList< Song * > & delSongs )
{
- for each Song in the list to remove
- find the row that contains it
- call beginRemoveRows()
- remove the Song * instance from the QList< Song * >
- call endRemoveRows()
}


(I think each call to endRemoveRows() will result in an update to the views, so there might be a cleaner way to do this that minimizes the number of updates).

Alternatively, if you store the song list externally to the model (and simply pass in a pointer for the model to use), you can have a custom method that completely replaces the data after the songlist is modified:



void MyModel::updateSongList( QList< Song * > * songlist )
{
beginResetModel();
mpSonglist = songlist;
endResetModel();
}


This mechanism has the downside that any currently selected entries in the model are cleared and the view is completely updated. The upside is that you are able to share the underlying song list among different models (say, a table view vs. a tree view) without duplicating the QList< Song * > and without the need to keep the copies of the QList stored in all the models in sync with each other.

I would probably do it the first way, by keeping duplicate lists if there are multiple models. It isn't so hard to keep them in sync if you have a central set of methods that handle it.

Jay-D
13th April 2013, 13:00
Thanks for all the answers.

This afternoon, I thought I could give QListWidget a try and it turned out that it does provide everything I want while being easier for me to understand. I appreciate especially your answers d_stranz, they will definitely come in handy when I start some other projects roaming around in my head, that won't work with the convenience classes. Somehow I thought the convenience classes were only suitable for the most basic cases, but considering the data I want to present is very limited, I should have tried QListWidget first, which would have saved me a lot of headaches.
Anyway, I'm completely satisfied with the QListWidget solution, so I consider this thread solved.
Thanks again.