Handling a Lot of Text in QML
I will be talking about text in this post, specifically about cases where you have to handle a lot of it. We are not talking about a general solution, but a specific case that we encountered during a customer project.
The Problem
The project involved showing a chat room for internal communication. We developed a functioning chat room and, eventually, as it grew, we also added support for limited chat history. This was all fine until we heard from the customer that users might need to browse through the whole history, which, in some cases, can span hundreds of thousands of lines of rich text. “How hard can it be?,” we thought. This was a QML application and we soon realized that it’s not as easy as we thought. To see why, consider this demo app:
It’s a very small, simple app which doesn’t even look like anything when you start it. Then, I hit a button to load the chat history and everything just gets stuck. It takes a while but, eventually, it loads.
Although it takes a few seconds to load and this is around 5000 files of HTML, our problems have only just begun. Even though it might feel smooth to scroll, sometimes it’s not. Then, you resize it and what happens? The whole thing gets stuck, starts responding for a while but not quite in the resize behavior we want.
The problem is that TextArea just can’t handle displaying so much text at once. Obviously, this was not ideal. We went back to the customer to ask if they really want to look at this big load of text and, of course, they said yes! Given that all this works fine in the widgets world, it’s not impossible.
Solutions
At this point, we started to try a few approaches. The first thing we tried was, obviously, profiling to see what’s going on. It turned out that resizing a text document is a lot of work. Imagine that you’re looking at a specific line in the text area. Now, every time you try to resize, it needs to figure out which line you should be at now. For that it needs to take into account formatting, word wrapping, and a couple of other things. All of that really takes a long time and everything just goes bonkers.
To remedy this, we tried a custom painted item and painting the document ourselves with some optimizations. We actually got it working and everything seemed fine at first. But then, we ran into other problems similar to the performance issues from the original TextArea.
The second thing we tried (which started as a joke, to be honest) was using WebEngineView since browsers are really good at handling text. So, why not? We just slammed a WebEngineView in there and loaded all the text. It loaded instantly because HTML browsers can just take it in a jiffy with no problem at all. That’s fine until you realize that the customer’s application actually shows 20 of these chat rooms, for example. So, you won’t just have one, you will have 20 WebEngineView instances, which would not really go kindly on the RAM and your users will hate you.
Eureka!
Then, we ended up doing what I’m going to demonstrate next. We essentially created a ListView, did some magic in the background, and everything seemed to work…
…well, sort of (maybe you can already tell what the problem might be?). Let’s peek under the hood to see how this is implemented:
ListView {
model: chatLogModel
delegate: TextArea {
padding: 0
width: ListView.view.width
textFormat: TextEdit.RichText
wrapMode: Text.Wrap
selectByKeyboard: true
selectByMouse: true
text: display
}
}
Instead of the whole thing being one fat TextArea, every delegate is one. This is very efficient at showing practically unlimited lines of text because ListView will only instantiate delegates for the lines that are visible. However, this means that selection no longer works properly; you cannot select across lines. Fear not! With a little bit of patience, it is possible to manually implement the selection behavior. If you are curious about the code, take a look here. We essentially figure out where the user is scrolling the mouse through a MouseArea on top of the ListView and then, in essentially all the delegates, find out what needs to be selected. This works surprisingly well.
(Tip: The demo handles selection, etc., in QML for ease of demonstration; in real world code, you’d be better doing this with a proper selection model in C++.)
Conclusion
As I stated at the beginning of this post, this is not a general solution. But for cases where just viewing the text is enough, the moral of the story is: ListView rocks!
If you like this article and want to read similar material, consider subscribing via our RSS feed.
Subscribe to KDAB TV for similar informative short video content.
KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.
Thanks for https://github.com/shaan7/qtdevcon2022-textarea/blob/main/main.qml#L59 this might become handy for our matrix-based chat app https://apps.kde.org/neochat/ 😀
In case someone wants to look at how we wrote a chat app in QML, you might want to look at https://invent.kde.org/network/neochat/-/tree/master/imports/NeoChat/Component/Timeline and https://invent.kde.org/network/neochat/-/blob/master/imports/NeoChat/Page/RoomPage.qml
I’m glad that you found it useful 🙂
I do remember trying to optimize Neochat’s ListView delegates a while back, but never got to finishing it. It seems it has improved quite a lot now, good job!
Cool trick! While ListView may still be appropriate for a chat app, there have been changes in Qt 6 that should improve performance when showing large text content (and more to come). The overarching epic (large content in general) is accessible at https://bugreports.qt.io/browse/QTBUG-90734 and has links to the current set of patches.
Just a heads-up! It might be interesting to see if these improvements affect your use case (and if not, if there is something that can be done about that.)
It’s creative. Congratulation @ShantanuTushar