I'm writing an application that displays a lot of text. It's not words and sentences though, it's binary data displayed in CP437 charset. Current form:
I'm having a problem though with drawing those characters. I need to draw each character one by one, because later I would like to apply different coloring. Those characters should have a transparent background as well, because later I would like to draw sections and ranges with different colors in the background (to group those characters based on some criteria).
The application supports multiple opened files at the same time, but when there are multiple files opened, the drawing starts to be noticeable on fast i7, so it's probably badly written.
What would be the best approach to draw this kind of data in Qt5? Should I just prerender characters to a bitmap and start from there, or it actually is possible to draw lots of characters by using normal Qt functions to draw text?
Edit: I'm using a normal QFrame
widget that does drawing in paintEvent
, using QPainter
. Is this a wrong approach? I've read some docs on QGraphicsScene
, from which I've remembered that it's best used in situations where a widget needs to have some control on the objects it draws. I don't need any control on what I draw; I just need to draw it and that's all. I won't reference any particular character after I'll draw it.
The widget has 2000 lines, so I won't paste the whole code, but currently my drawing approach is like this:
- First, create a table (
cache
) with 256 entries, put the iterator counter toi
variable, - For each entry, create a
QStaticText
object that contains drawing information about a character identified by ASCII code taken fromi
variable, - Later, in the drawing function, for each byte in the input stream (i.e. from the file), draw the data using
QStaticText
from thecache
table. So, to draw ASCII character0x7A
, I'll look upQStaticText
from index0x7a
incache
table, and feed thisQStaticText
object into theQPainter
object.
I was also experimenting with a different approach, rendering the whole line in one QPainter::drawText
call, and indeed it was faster, but I've lost possibility of coloring each character with different color. I would like to have this possibility.
One solution I sometimes use is to keep a cache of pre-rendered lines. I normally use a doubly-linked LRU list of entries with about twice the lines that can be seen on the screen. Every time a line is used for rendering is moved to the front of the list; when I need to create a new line and the current cache count is past the limit I reuse the last entry in the list.
By storing the final result of individual lines you can repaint the display very quickly as probably in many cases most of the lines will not change from one frame to the next (including when scrolling).
The increased complexity is also reasonably confined in having to invalidate the line when you change the content.
The use of a
QGraphicsScene
wouldn't improve things - it's an additional layer on top of aQWidget
. You're after raw performance, so you shouldn't be using it.You could implement a
QTextDocument
as a viewmodel for the visible section of your memory buffer/file, but painting the freshQTextDocument
each time you scroll wouldn't be any faster than drawing things directly on aQWidget
.Using
QStaticText
is a step in the right direction, but insufficient: renderingQStaticText
still requires the rasterization of the glyph's shape. You can do better and cache the pixmap of eachQChar, QColor
combination that you wish to render: this will be much faster than rasterizing character outlines, whether usingQStaticText
or not.Instead of drawing individual characters, you then draw pixmaps from the cache. This commit demonstrates this approach. The character drawing method is:
You could also cache each (character,foreground,background) tuple. Alas, this gets quickly out of hand when there are many foreground/background combinations.
If all of your backgrounds are of the same color (e.g. white), you'd wish to store a negative mask of the character: the
glyph
has a white background and a transparent shape. This commit demonstrates this approach. The glyph rectangle is filled with glyph color, then a white mask is applied on top:Instead of storing a fully pre-rendered character of a given color, you could store just the alpha mask and composite them on-demand:
CompositionMode_Source
).CompositionMode_SourceOut
: the background will remain with a hole for the character itself.CompositionMode_DestinationOver
: the foreground will fill the hole.This turns out to be reasonably fast, and the rendering is fully parallelizable - see the example below.
Note: The pre-rendered glyph could use further premultiplication of the color with alpha to appear less thick.
Yet another approach, with excellent performance, would be to emulate a text-mode display using the GPU. Store the pre-rendered glyph outlines in a texture, store the glyph indices and colors to be rendered in an array, and use OpenGL and two shaders to do the rendering. This example might be a starting point to implement such an approach.
A complete example, using CPU rendering across multiple threads, follows.
We start with the backing store view, used to produce
QImage
s that are views into the backing store for a given widget, and can be used to parallelize painting.On a 2013 iMac, this code repaints the full-screen widget in about 8ms.
Then, the CP437 character set:
The
HexView
widget derives fromQAbstractScrollArea
and visualizes a memory-mapped chunk of data:The parallelized rendering is done in class methods - they don't modify the state of the widget, other than accessing read-only data, and rendering into the backing store. The threads each act on isolated lines in the store.
This method distributes the chunks of work between threads:
The remainder of the implementation is uncontroversial:
We leverage modern 64-bit systems and memory-map the source file to be visualized by the widget. For test purposes, a view of the character set is also available: