Adjust the height of QListView to fit the content - c++

I want the height of a QListView based on an QAbstractListModel to fit the contents, if the amount of the items is smaller a given number N. If there are more than N items, it should show only N items. I read a lot of curious tips in the web, but most of them look like hacks. I guess this has something to do with sizeHint() but in the model view approach there is no ItemWidget in which I could override the sizeHint(). What is the correct way to achieve this behaviour?
Further, how does this correlate to the size policy of the parent app? This is a second constraint: The contents should not try to use the space they have in the parent widget, but the parent widget should resize to fit the QListView.
This is not a duplicate to this question, since I can't use QCompleter.

sizeHint() has to be overridden in the QListView (respectively your subclass of it). The mentioned special behaviour can be implemented there. eg like this:
QSize ProposalListView::sizeHint() const
{
if (model()->rowCount() == 0) return QSize(width(), 0);
int nToShow = _nItemsToShow < model()->rowCount() ? _nItemsToShow : model()->rowCount();
return QSize(width(), nToShow*sizeHintForRow(0));
}
This requires the size hint of the item delegate to be reasonable. In my case:
inline QSize sizeHint ( const QStyleOptionViewItem&, const QModelIndex& ) const override { return QSize(200, 48); }
Now I just have to call updateGeometry() after changing the model.

For those who use ListView in QML with QtQuick Controls.
I made the content fit with property anchors.bottomMargin.
anchors.bottomMargin: 20

I have faced the same problem. The solution marked as answer doesn't work correctly when you have scroll bars or you set a frame border. So here is my solution, which is up to date, that works for all QAbstractItemViews.
Since Qt 5.2 version QAbstractItemView has method setSizeAdjustPolicy, inherited from QAbstractScrollArea. If you set QAbstractScrollArea::AdjustToContents than the scroll area will always adjust to the viewport (content).
But the view by default can not be smaller than it's minimum size hint's. So here is what should be done to make an item view fully shrink when the model doesn't have any items:
Set setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents)
Override minimumSizeHint:
QSize minimumSizeHint() const override {
return QSize(0, 0);
}
Override viewportSizeHint (when model has no items, QTreeView and QListView has a default size hint for the viewport, so need to return (0,0) in those cases):
QSize viewportSizeHint() const override {
if (QAbstractItemView::sizeAdjustPolicy() != QAbstractScrollArea::AdjustToContents)
return T::viewportSizeHint();
// if QTableView is used, comment the block below
if (model() == nullptr)
return QSize(0, 0);
if (model()->rowCount() == 0)
return QSize(0, 0);
// T is your view type (QTreeView, QTableView, QListView, etc.)
return T::viewportSizeHint();
}
NOTE:
Don't forget to set sizePolicy or add stretch to the layout.
Not hidden scroll bars increase viewport's minimum size.
Also QListView had problems (bug report) with AdjustToContents flag, that were fixed since Qt 6.2. So if you use a version below 6.2 override viewportSizeHint like this:
QSize viewportSizeHint() const override
{
if (QAbstractItemView::sizeAdjustPolicy() != QAbstractScrollArea::AdjustToContents)
return T::viewportSizeHint();
if (std::is_same<T, QTreeView>::value || std::is_same<T, QListView>::value)
{
if (model() == nullptr)
return QSize(0, 0);
if (model()->rowCount() == 0 || model()->columnCount() == 0)
return QSize(0, 0);
}
if (std::is_same<T, QListView>::value)
{
const int rowCount = model()->rowCount();
int height = 0;
for (int i = 0; i < rowCount; i++) {
height += T::sizeHintForRow(i);
}
return QSize(T::viewportSizeHint().width(), height);
}
return T::viewportSizeHint();
}

There is no good way to do this. I use the following code.
Header:
class List_view_auto_height : public QObject {
Q_OBJECT
public:
explicit List_view_auto_height(QListView * target_list);
void set_max_auto_height(int value);
void set_min_height(int value);
private:
QListView* list;
QTimer timer;
int _min_height;
int _max_height;
bool eventFilter(QObject* object, QEvent* event);
private slots:
void update_height();
};
Source:
List_view_auto_height::List_view_auto_height(QListView *target_list) :
QObject(target_list)
, list(target_list)
{
_min_height = 0;
_max_height = 250;
connect(list->model(), &QAbstractItemModel::rowsInserted,
this, &List_view_auto_height::update_height);
connect(list->model(), &QAbstractItemModel::rowsRemoved,
this, &List_view_auto_height::update_height);
connect(list->model(), &QAbstractItemModel::layoutChanged,
this, &List_view_auto_height::update_height);
list->installEventFilter(this);
update_height();
connect(&timer, &QTimer::timeout, this, &List_view_auto_height::update_height);
timer.start(500);
}
void List_view_auto_height::set_max_auto_height(int value) {
_max_height = value;
update_height();
}
void List_view_auto_height::set_min_height(int value) {
_min_height = value;
update_height();
}
bool List_view_auto_height::eventFilter(QObject *object, QEvent *event) {
if (event->type() == QEvent::Show) {
update_height();
}
return false;
}
void List_view_auto_height::update_height() {
if (!list->isVisible()) { return; }
int height = 0;
if (list->model()->rowCount() > 0) {
height = list->visualRect(list->model()->index(list->model()->rowCount() - 1, 0)).bottom() + 1;
height -= list->visualRect(list->model()->index(0, 0)).top();
}
if (list->horizontalScrollBar()->isVisible()) {
height += list->horizontalScrollBar()->height();
}
bool scrollbar_enabled = false;
if (_max_height != 0 && height > _max_height) {
height = _max_height;
scrollbar_enabled = true;
}
if (height < _min_height) {
height = _min_height;
}
list->setFixedHeight(height + 6);
}
Usage:
new List_widget_auto_height(list);
It's full of hacks and can work incorrectly in some cases. Feel free to improve it.
It sets height using setFixedHeight. This should provide correct behavior for parent widget's size hint.

Related

QCombobox Going up and down knob change

I use physical Knob for control menu than included QCombobox. When press knob then call click function of ComboBoxObject. When knob rotate then rotate function call with value {-1,1}.
I need change highlighted item when QComboBox is popup in rotate function.
void ComboBoxObject::click(){
showPopup();
}
void ComboBoxObject::rotate(direct int) // Left=-1, Right=1
{
up/down
}
When focus on Combobox setCurrenIndex(index) work correct but if pupop list then it doesn't work. I use highlighted(index) but not work.
I try simulate key_up and key_dow for change highlighted item. highlighted item doesn't change.
void ComboBoxObject::rotate(direct int) // Left=-1, Right=1
{
QKeyEvent *event;
if(direct == 1){
event = new QKeyEvent ( QEvent::KeyPress, Qt::Key_Up,Qt::NoModifier);
}
else{
event = new QKeyEvent ( QEvent::KeyPress, Qt::Key_Down,Qt::NoModifier);
}
bool result = QApplication::sendEvent(this,event);
}
QComboBox doesn't have that functionality built in, but you can implement it yourself easily enough:
void rotateComboBox(QComboBox * theComboBox, int direction)
{
const int numItems = theComboBox->count();
const int curIndex = theComboBox->currentIndex();
int newIndex = curIndex+direction;
if (newIndex <= 0) newIndex = numItems-1; // wrap around
if (newIndex >= numItems) newIndex = 0; // wrap around
theComboBox->setCurrentIndex(newIndex);
}

Qt C++ QVector QLine QPoint drawingtool

I have QVector m_vertices and QVector m_lines in my drawingWidget.h
I currently trying to do simple drawing tool, where i can manipulate with vertices and lines. I managed to draw multiple vertices on my "MainWindow", now i am trying to implement line between two vertices
The main idea is, that i have a pop-up menu, where i can choose a "tool". I can add vertex, remove vertex, move vertex, add line, delete line. The idea is , when i choose for example "Add Vertex" then the "m_state" will change to "Adding" so i can only add vertices. When i choose "Add Line" for example, then the "m_state" again will change, so that i can only choose two created vertices, that will result a line between them.
enum DrawingWidgetState {
NO_TOOL_SELECTED,
ADD_VERTEX_SELECTED,
MOVE_VERTEX_SELECTED,
DELETE_VERTEX_SELECTED,
ADD_LINE_SELECTED,
DELETE_LINE_SELECTED
};
Here, in the end, it turns out that if I click "Add Vertex" then when I put the second vertex, then it automatically puts a line between them
void DrawingWidget::paintEvent(QPaintEvent *event) {
QPainter painter(this);
painter.fillRect(event->rect(), Qt::blue);
painter.setBrush(Qt::black);
for(int i = 0; i < m_vertices.size() ; i++) {
painter.drawEllipse(m_vertices[i], 20, 20);
}
for(int i = 0; i < m_vertices.size()-1 ; i++)
{
const QPoint& point1 = m_vertices[i];
const QPoint& point2 = m_vertices[i+1];
painter.drawLine(point1, point2);
}
}
void DrawingWidget::mousePressEvent(QMouseEvent *event) {
if(m_state == ADD_VERTEX_SELECTED) {
if(event->button() == Qt::LeftButton) {
//m_x = event->x();
//m_y = event->y();
//update();
QPoint point = event->pos();
m_vertices.append(point);
update();
}
}
if(m_state == ADD_LINE_SELECTED) {
if(event->button() == Qt::LeftButton) {
for(int i = 0; i < m_vertices.size()-1 ; i++) {
const auto &point1 = m_vertices[i];
const auto &point2 = m_vertices[i+1];
}
}
}
}
Could someone explain to me why this happens and what should i do to improve my code ?
If something requires explanation, i will tell.

Rotate QImage in QListWidget

Got a small app where in a QListWidget many QImage and text are displayed with this function:
void TileDisplay::DisplayTiles()
{
for(int i = 0; i < m_tiles.size(); i++) {
QListWidgetItem *item = new QListWidgetItem("Tile " + QString::number(i+1), ui->listWidget);
item->setData(Qt::DecorationRole, m_tiles[i].scaled(64, 64, Qt::IgnoreAspectRatio, Qt::FastTransformation));
}
}
I'm looking for a solution where the QImage is rotated around it's centre with this function:
void TileDisplay::RotateImage(int degree)
{
if(GetTiles().size() > 0) {
QImage *tileToRotate = GetCurrentTile();
if(tileToRotate != nullptr) {
QTransform rotate;
rotate.rotate(degree);
tileToRotate->transformed(rotate);
DisplayTiles();
}
}
}
It is running without any issue, but the image is not rotated.
Any help appreciated.
From the documentation the signature of the QImage::transformed member function is...
QImage QImage::transformed(const QTransform &matrix, Qt::TransformationMode mode) const;
So you need to make use of the returned QImage. Try...
*tileToRotate = tileToRotate->transformed(rotate);

Dynamically change font size of QLabel to fit available space

I'm trying to make an horizontal layout with 3 QLabel scale use all its available space. More specifically, this is what I have
this is what I am aiming for
At the moment, the second image is achieved by changing the stylesheet of the qlabels with a slider. Additionally, since I have the the three labels in a layout inside a groupbox, the groupbox resizes to fit its contents, cool.
Now I wanted to drop the slider approach and instead autofit the space available when moving the splitters. In this question, OP reimplements the resizeEvent, and I've seen other posts suggesting the same, changing point by point with this while( !doesFit ) or something similar.
I tried using this approach, both on the resize event and on the splitterMoved event. However, this approach is way prone to feedback loops and other display errors caused. In the other question, they suggest enabling ignoreSizePolicy to prevent the size policy retriggering the sizeevent, but I like how qt handles the size of the layout, how it keeps a minimum size and then it folds the widget if the user insists. Maybe it would work if the HLayout would ignore the resize events triggered by the QLabels, still IMHO unclean thought.
I was wondering if that's the recommended way of achieving this, and wether a less unstable solution exists, maybe using using stylesheets. There are some behaviours that I could also drop, the minimum size limit (so the user could potentially hide the groupbox).
If that's the recommended way of doing it, how should I use the fontmetrics if I have three separate labels, one of which (the number) changes its text dynamically and rapidly? It should not have an impact on performance, and that while loop makes me wary.
It doesn't sound like the while(!fit) approach is going to cut it. Or does it?
--- Edit regarding the duplicate question
Another post creates an event filter, which might also work if reworked to deal with a layout with 3 labels. I finally used a version of the first mentioned post with the variation of the post mentioned in the comments. I'll post the answer if the question is reopened.
One could apply the Newton's method approach from this answer to work on all widgets in a given layout. It will work on any widget with a settable font, not only on a QLabel.
The Newton's algorithm converges reasonably quickly when given a good starting point, e.g. when resizing interactively. It's not atypical to have the loop execute only once. On the other hand, QWidget::sizeHint is integer-valued and and widgets may round fractional font sizes, thus sometimes the iteration is a bit slower than one would expect. The number of iterations is capped to ensure decent performance.
A custom replacement for the label, that provided a QSizeF sizeHintF(), would work better here.
The minimum sizing for the widgets is a bit of a stretch, as the size is not updated as the widget contents change. This could be remedied easily, though.
// https://github.com/KubaO/stackoverflown/tree/master/questions/label-text-size-vert-40861305
#include <QtWidgets>
class LabelStretcher : public QObject {
Q_OBJECT
static constexpr const char kMinimumsAcquired[] = "ls_minimumsAcquired";
static constexpr const char kStretcherManaged[] = "ls_stretcherManaged";
public:
LabelStretcher(QObject *parent = 0) : QObject(parent) {
apply(qobject_cast<QWidget*>(parent));
}
void apply(QWidget *widget) {
if (!widget) return;
setManaged(widget);
setMinimumSize(widget);
widget->installEventFilter(this);
}
void setManaged(QWidget *w, bool managed = true) {
w->setProperty(kStretcherManaged, managed);
}
protected:
bool eventFilter(QObject * obj, QEvent * ev) override {
auto widget = qobject_cast<QWidget*>(obj);
if (widget && ev->type() == QEvent::Resize)
resized(widget);
return false;
}
private:
void onLayout(QLayout *layout, const std::function<void(QWidget*)> &onWidget) {
if (!layout) return;
auto N = layout->count();
for (int i = 0; i < N; ++i) {
auto item = layout->itemAt(i);
onWidget(item->widget());
onLayout(item->layout(), onWidget);
}
}
void setFont(QLayout *layout, const QFont &font) {
onLayout(layout, [&](QWidget *widget){ setFont(widget, font); });
}
void setFont(QWidget *widget, const QFont &font) {
if (!widget || !widget->property(kStretcherManaged).toBool()) return;
widget->setFont(font);
setFont(widget->layout(), font);
}
void setMinimumSize(QWidget *widget) {
if (widget->layout()) return;
widget->setMinimumSize(widget->minimumSizeHint());
}
static int dSize(const QSizeF & inner, const QSizeF & outer) {
auto dy = inner.height() - outer.height();
auto dx = inner.width() - outer.width();
return std::max(dx, dy);
}
qreal f(qreal fontSize, QWidget *widget) {
auto font = widget->font();
font.setPointSizeF(fontSize);
setFont(widget, font);
auto d = dSize(widget->sizeHint(), widget->size());
qDebug() << "f:" << fontSize << "d" << d;
return d;
}
qreal df(qreal fontSize, qreal dStep, QWidget *widget) {
fontSize = std::max(dStep + 1.0, fontSize);
return (f(fontSize + dStep, widget) - f(fontSize - dStep, widget)) / dStep;
}
void resized(QWidget *widget) {
qDebug() << "pre: " << widget->minimumSizeHint() << widget->sizeHint() << widget->size();
if (!widget->property(kMinimumsAcquired).toBool()) {
onLayout(widget->layout(), [=](QWidget *widget){ setMinimumSize(widget); });
widget->setProperty(kMinimumsAcquired, true);
}
// Newton's method
auto font = widget->font();
auto fontSize = font.pointSizeF();
qreal dStep = 1.0;
int i;
for (i = 0; i < 10; ++i) {
auto prevFontSize = fontSize;
auto d = df(fontSize, dStep, widget);
if (d == 0) {
dStep *= 2.0;
continue;
}
fontSize -= f(fontSize, widget)/d;
fontSize = std::max(dStep + 1.0, fontSize);
auto change = fabs(prevFontSize - fontSize)/fontSize;
qDebug() << "d:" << d << " delta" << change;
if (change < 0.01) break; // we're within 1% of target
}
font.setPointSizeF(fontSize);
setFont(widget, font);
qDebug() << "post:" << i << widget->minimumSizeHint() << widget->sizeHint() << widget->size();
}
};
constexpr const char LabelStretcher::kMinimumsAcquired[];
constexpr const char LabelStretcher::kStretcherManaged[];
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QWidget w;
QGridLayout layout{&w};
LabelStretcher stretch{&w};
QLabel labels[6];
QString texts[6] = {"V", "30.0", "kts", "H", "400.0", "ft"};
int i = 0, j = 0, k = 0;
for (auto & label : labels) {
stretch.setManaged(&label);
label.setFrameStyle(QFrame::Box);
label.setText(texts[k++]);
if (j == 0) label.setAlignment(Qt::AlignRight | Qt::AlignVCenter);
else if (j == 1) label.setAlignment(Qt::AlignCenter);
layout.addWidget(&label, i, j++);
if (j >= 3) { i++; j=0; }
}
w.show();
return app.exec();
}
#include "main.moc"
Althought I consider KubaOber's answer better, I'll post this in case it's helpful to someone who wants a solution in the line of the answers mentioned in the post.
Note that the sampletext could be retrieved from the labels as well, the font from the stylesheet, and the code could potentially be placed on a resizeEvent of the groupbox or layout. It wouldn't work on the resizeEvent of the labels since they would compete for the space.
That is one reason why KubaOber answer is superior. Other reasons I can think of is stability given that the 3 labels space differs from the sampletext, thus the font size is not as accurate as it could be. Therefore a resize event could potentially be triggered again by the font change.
static void fitGroupBoxLabels(QGroupBox* groupbox, const QFont &samplefont, const QLayout* const samplelayout)
{
groupbox->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
QString sampletext = "V 1000.0 kts";
QRect availablerect = samplelayout->contentsRect();
if(samplefont.pointSizeF() <= 0) return; //not initalized yet, return
QRect textrect = QFontMetrics(samplefont).boundingRect(sampletext);
if(!textrect.isValid() || !availablerect.isValid()) return; //not initalized yet, return
float factorh = availablerect.width() / (float)textrect.width();
float factorw = availablerect.height() / (float)textrect.height();
float factor = std::min(factorh, factorw);
if (factor < 0.95 || factor > 1.05)
{
float fontSize = samplefont.pointSizeF()*factor;
QString groupBoxStyle = QString("QGroupBox{font-size:8pt} QLabel{font-size:%1pt}").arg(fontSize);
groupbox->setStyleSheet(groupBoxStyle);
}
}
After struggling with this issue, I create DynamicFontSizeLabel and DynamicFontSizePushButton widgets. Hope it helps.
https://github.com/jonaias/DynamicFontSizeWidgets/

How to add lines numbers to QTextEdit?

I am writing a Visual Basic IDE, and I need to add lines numbers to QTextEdit and highlight current line. I have found this tutorial, but it is written in Java and I write my project in C++.
I know that Qt tutorial recommends using QPlainTextEdit for text editor implementations, and that the question (except as mentioned in the title), is more general than dealing (absolutely) with a QTextEdit widget, but I succeeded in implementing the behaviour (line numbers + current line number highlight), and I think this might be helpful for some people (like me) who really want to keep going with the Rich Text widget, and want to share my implementation (which is far from perfect - quite fast coded...).
LineNumberArea.h : (Same as "QPlainTextEdit" tutorial)
class LineNumberArea : public QWidget
{
Q_OBJECT
public:
LineNumberArea(QTextEdit *editor);
QSize sizeHint() const;
protected:
void paintEvent(QPaintEvent *event);
private:
QTextEdit *codeEditor;
};
LineNumberArea.cpp : (Same as "QPlainTextEdit" tutorial)
LineNumberArea::LineNumberArea(QTextEdit *editor) : QWidget(editor) {
codeEditor = editor;
}
QSize LineNumberArea::sizeHint() const {
return QSize(((QTextEditHighlighter *)codeEditor)->lineNumberAreaWidth(), 0);
}
void LineNumberArea::paintEvent(QPaintEvent *event) {
((QTextEditHighlighter *)codeEditor)->lineNumberAreaPaintEvent(event);
}
>> qtextedithighlighter.h :
class QTextEditHighlighter : public QTextEdit
{
Q_OBJECT
public:
explicit QTextEditHighlighter(QWidget *parent = 0);
int getFirstVisibleBlockId();
void lineNumberAreaPaintEvent(QPaintEvent *event);
int lineNumberAreaWidth();
signals:
public slots:
void resizeEvent(QResizeEvent *e);
private slots:
void updateLineNumberAreaWidth(int newBlockCount);
void updateLineNumberArea(QRectF /*rect_f*/);
void updateLineNumberArea(int /*slider_pos*/);
void updateLineNumberArea();
private:
QWidget *lineNumberArea;
};
>> qtextedithighlighter.cpp :
#include "qtextedithighlighter.h"
QTextEditHighlighter::QTextEditHighlighter(QWidget *parent) :
QTextEdit(parent)
{
// Line numbers
lineNumberArea = new LineNumberArea(this);
///
connect(this->document(), SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
connect(this->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(updateLineNumberArea/*_2*/(int)));
connect(this, SIGNAL(textChanged()), this, SLOT(updateLineNumberArea()));
connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(updateLineNumberArea()));
///
updateLineNumberAreaWidth(0);
}
int QTextEditHighlighter::lineNumberAreaWidth()
{
int digits = 1;
int max = qMax(1, this->document()->blockCount());
while (max >= 10) {
max /= 10;
++digits;
}
int space = 13 + fontMetrics().width(QLatin1Char('9')) * (digits);
return space;
}
void QTextEditHighlighter::updateLineNumberAreaWidth(int /* newBlockCount */)
{
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
}
void QTextEditHighlighter::updateLineNumberArea(QRectF /*rect_f*/)
{
QTextEditHighlighter::updateLineNumberArea();
}
void QTextEditHighlighter::updateLineNumberArea(int /*slider_pos*/)
{
QTextEditHighlighter::updateLineNumberArea();
}
void QTextEditHighlighter::updateLineNumberArea()
{
/*
* When the signal is emitted, the sliderPosition has been adjusted according to the action,
* but the value has not yet been propagated (meaning the valueChanged() signal was not yet emitted),
* and the visual display has not been updated. In slots connected to this signal you can thus safely
* adjust any action by calling setSliderPosition() yourself, based on both the action and the
* slider's value.
*/
// Make sure the sliderPosition triggers one last time the valueChanged() signal with the actual value !!!!
this->verticalScrollBar()->setSliderPosition(this->verticalScrollBar()->sliderPosition());
// Since "QTextEdit" does not have an "updateRequest(...)" signal, we chose
// to grab the imformations from "sliderPosition()" and "contentsRect()".
// See the necessary connections used (Class constructor implementation part).
QRect rect = this->contentsRect();
lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
updateLineNumberAreaWidth(0);
//----------
int dy = this->verticalScrollBar()->sliderPosition();
if (dy > -1) {
lineNumberArea->scroll(0, dy);
}
// Addjust slider to alway see the number of the currently being edited line...
int first_block_id = getFirstVisibleBlockId();
if (first_block_id == 0 || this->textCursor().block().blockNumber() == first_block_id-1)
this->verticalScrollBar()->setSliderPosition(dy-this->document()->documentMargin());
// // Snap to first line (TODO...)
// if (first_block_id > 0)
// {
// int slider_pos = this->verticalScrollBar()->sliderPosition();
// int prev_block_height = (int) this->document()->documentLayout()->blockBoundingRect(this->document()->findBlockByNumber(first_block_id-1)).height();
// if (dy <= this->document()->documentMargin() + prev_block_height)
// this->verticalScrollBar()->setSliderPosition(slider_pos - (this->document()->documentMargin() + prev_block_height));
// }
}
void QTextEditHighlighter::resizeEvent(QResizeEvent *e)
{
QTextEdit::resizeEvent(e);
QRect cr = this->contentsRect();
lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
}
int QTextEditHighlighter::getFirstVisibleBlockId()
{
// Detect the first block for which bounding rect - once translated
// in absolute coordinated - is contained by the editor's text area
// Costly way of doing but since "blockBoundingGeometry(...)" doesn't
// exists for "QTextEdit"...
QTextCursor curs = QTextCursor(this->document());
curs.movePosition(QTextCursor::Start);
for(int i=0; i < this->document()->blockCount(); ++i)
{
QTextBlock block = curs.block();
QRect r1 = this->viewport()->geometry();
QRect r2 = this->document()->documentLayout()->blockBoundingRect(block).translated(
this->viewport()->geometry().x(), this->viewport()->geometry().y() - (
this->verticalScrollBar()->sliderPosition()
) ).toRect();
if (r1.contains(r2, true)) { return i; }
curs.movePosition(QTextCursor::NextBlock);
}
return 0;
}
void QTextEditHighlighter::lineNumberAreaPaintEvent(QPaintEvent *event)
{
this->verticalScrollBar()->setSliderPosition(this->verticalScrollBar()->sliderPosition());
QPainter painter(lineNumberArea);
painter.fillRect(event->rect(), Qt::lightGray);
int blockNumber = this->getFirstVisibleBlockId();
QTextBlock block = this->document()->findBlockByNumber(blockNumber);
QTextBlock prev_block = (blockNumber > 0) ? this->document()->findBlockByNumber(blockNumber-1) : block;
int translate_y = (blockNumber > 0) ? -this->verticalScrollBar()->sliderPosition() : 0;
int top = this->viewport()->geometry().top();
// Adjust text position according to the previous "non entirely visible" block
// if applicable. Also takes in consideration the document's margin offset.
int additional_margin;
if (blockNumber == 0)
// Simply adjust to document's margin
additional_margin = (int) this->document()->documentMargin() -1 - this->verticalScrollBar()->sliderPosition();
else
// Getting the height of the visible part of the previous "non entirely visible" block
additional_margin = (int) this->document()->documentLayout()->blockBoundingRect(prev_block)
.translated(0, translate_y).intersect(this->viewport()->geometry()).height();
// Shift the starting point
top += additional_margin;
int bottom = top + (int) this->document()->documentLayout()->blockBoundingRect(block).height();
QColor col_1(90, 255, 30); // Current line (custom green)
QColor col_0(120, 120, 120); // Other lines (custom darkgrey)
// Draw the numbers (displaying the current line number in green)
while (block.isValid() && top <= event->rect().bottom()) {
if (block.isVisible() && bottom >= event->rect().top()) {
QString number = QString::number(blockNumber + 1);
painter.setPen(QColor(120, 120, 120));
painter.setPen((this->textCursor().blockNumber() == blockNumber) ? col_1 : col_0);
painter.drawText(-5, top,
lineNumberArea->width(), fontMetrics().height(),
Qt::AlignRight, number);
}
block = block.next();
top = bottom;
bottom = top + (int) this->document()->documentLayout()->blockBoundingRect(block).height();
++blockNumber;
}
}
Hope this can help...
Here's the equivalent tutorial in C++:
Qt4: http://doc.qt.io/qt-4.8/qt-widgets-codeeditor-example.html
Qt5: http://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html
I was looking for a line numbers painting solution for QTextEdit (not QPlainTextEdit), and I found the previous answer with sample code for QTextEdit is useful, but when we set custom line height in QTextEdit's associated SyntaxHighligher, it doesn't work reliably.
To fix that problem, I figured out a simpler way to determine the y coordinate of each block rect by using this code:
// Here is the key to obtain the y coordinate of the block start
QTextCursor blockCursor(block);
QRect blockCursorRect = this->cursorRect(blockCursor);
And then we can draw line number of each block via:
painter.drawText(-5, blockCursorRect.y() /* + a little offset to align */,
m_lineNumberArea->width(), fixedLineHeight,
Qt::AlignRight, number);
This seems much simpler and more reliable than calculating the block y coordinate by adding previous block height up.
Hope it helps for someone who is looking for similar solutions.
Here is an easy example to determine the text positions for drawing line numbers for the QTextEdit derived classes.
code_browser::code_browser(QWidget *parent): QTextBrowser(parent)
, m_line_number_area(new LineNumberArea(this))
, m_show_line_numbers(false)
{
connect(document(), SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
connect(verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(vertical_scroll_value(int)));
connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int)));
connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine()));
updateLineNumberAreaWidth(blockCount());
highlightCurrentLine();
}
void code_browser::vertical_scroll_value(int value)
{
Q_EMIT updateRequest(contentsRect(), value);
}
void code_browser::lineNumberAreaPaintEvent(QPaintEvent *event)
{
QPainter painter(m_line_number_area);
painter.fillRect(event->rect(), Qt::lightGray);
int top = 0;
QTextBlock block = firstVisibleBlock(top);
int blockNumber = block.blockNumber();
QRectF block_rect = blockBoundingRect(block);
int bottom = top + qRound(block_rect.height());
while (block.isValid() && top <= event->rect().bottom())
{
if (block.isVisible() && bottom >= event->rect().top())
{
QString number = QString::number(blockNumber + 1);
painter.setPen(Qt::black);
painter.drawText(0, top, m_line_number_area->width(), fontMetrics().height(), Qt::AlignRight, number);
}
block = block.next();
top = bottom;
block_rect = blockBoundingRect(block);
bottom = top + qRound(block_rect.height());
++blockNumber;
}
}
int code_browser::blockCount() const
{
return document()->blockCount();
}
QTextBlock code_browser::firstVisibleBlock(int& diff)
{
QPointF content_offset = contentOffset();
for (QTextBlock block = document()->begin(); block.isValid(); block = block.next())
{
if (block.isVisible())
{
QRectF block_rect = blockBoundingRect(block);
if (block_rect.top() >= content_offset.y())
{
diff = block_rect.top() - content_offset.y();
return block;
}
}
}
diff = -1;
return document()->begin();
}
QRectF code_browser::blockBoundingRect(const QTextBlock &block) const
{
QAbstractTextDocumentLayout *layout = document()->documentLayout();
return layout->blockBoundingRect(block);
}
QPointF code_browser::contentOffset() const
{
return QPointF(horizontalScrollBar()->value(), verticalScrollBar()->value());
}
Python's adaptation:
QTextEditHighlighter.py
from PySide6.QtCore import QRectF, QRect, Qt
from PySide6.QtGui import QResizeEvent, QTextCursor, QPaintEvent, QPainter, QColor
from PySide6.QtWidgets import QTextEdit, QApplication
from LineNumberArea import LineNumberArea
class QTextEditHighlighter(QTextEdit):
def __init__(self):
# Line numbers
QTextEdit.__init__(self)
self.lineNumberArea = LineNumberArea(self)
self.document().blockCountChanged.connect(self.updateLineNumberAreaWidth)
self.verticalScrollBar().valueChanged.connect(self.updateLineNumberArea)
self.textChanged.connect(self.updateLineNumberArea)
self.cursorPositionChanged.connect(self.updateLineNumberArea)
self.updateLineNumberAreaWidth(0)
def lineNumberAreaWidth(self):
digits = 1
m = max(1, self.document().blockCount())
while m >= 10:
m /= 10
digits += 1
space = 13 + self.fontMetrics().horizontalAdvance('9') * digits
return space
def updateLineNumberAreaWidth(self, newBlockCount: int):
self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)
def updateLineNumberAreaRect(self, rect_f: QRectF):
self.updateLineNumberArea()
def updateLineNumberAreaInt(self, slider_pos: int):
self.updateLineNumberArea()
def updateLineNumberArea(self):
"""
When the signal is emitted, the sliderPosition has been adjusted according to the action,
but the value has not yet been propagated (meaning the valueChanged() signal was not yet emitted),
and the visual display has not been updated. In slots connected to self signal you can thus safely
adjust any action by calling setSliderPosition() yourself, based on both the action and the
slider's value.
"""
# Make sure the sliderPosition triggers one last time the valueChanged() signal with the actual value !!!!
self.verticalScrollBar().setSliderPosition(self.verticalScrollBar().sliderPosition())
# Since "QTextEdit" does not have an "updateRequest(...)" signal, we chose
# to grab the imformations from "sliderPosition()" and "contentsRect()".
# See the necessary connections used (Class constructor implementation part).
rect = self.contentsRect()
self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), rect.height())
self.updateLineNumberAreaWidth(0)
dy = self.verticalScrollBar().sliderPosition()
if dy > -1:
self.lineNumberArea.scroll(0, dy)
# Addjust slider to alway see the number of the currently being edited line...
first_block_id = self.getFirstVisibleBlockId()
if first_block_id == 0 or self.textCursor().block().blockNumber() == first_block_id-1:
self.verticalScrollBar().setSliderPosition(dy-self.document().documentMargin())
# # Snap to first line (TODO...)
# if first_block_id > 0:
# slider_pos = self.verticalScrollBar().sliderPosition()
# prev_block_height = (int) self.document().documentLayout().blockBoundingRect(self.document().findBlockByNumber(first_block_id-1)).height()
# if (dy <= self.document().documentMargin() + prev_block_height)
# self.verticalScrollBar().setSliderPosition(slider_pos - (self.document().documentMargin() + prev_block_height))
def resizeEvent(self, event: QResizeEvent):
QTextEdit.resizeEvent(self, event)
cr = self.contentsRect()
self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(), self.lineNumberAreaWidth(), cr.height()))
def getFirstVisibleBlockId(self) -> int:
# Detect the first block for which bounding rect - once translated
# in absolute coordinated - is contained by the editor's text area
# Costly way of doing but since "blockBoundingGeometry(...)" doesn't
# exists for "QTextEdit"...
curs = QTextCursor(self.document())
curs.movePosition(QTextCursor.Start)
for i in range(self.document().blockCount()):
block = curs.block()
r1 = self.viewport().geometry()
r2 = self.document().documentLayout().blockBoundingRect(block).translated(
self.viewport().geometry().x(), self.viewport().geometry().y() - (
self.verticalScrollBar().sliderPosition()
)).toRect()
if r1.contains(r2, True):
return i
curs.movePosition(QTextCursor.NextBlock)
return 0
def lineNumberAreaPaintEvent(self, event: QPaintEvent):
self.verticalScrollBar().setSliderPosition(self.verticalScrollBar().sliderPosition())
painter = QPainter(self.lineNumberArea)
painter.fillRect(event.rect(), Qt.lightGray)
blockNumber = self.getFirstVisibleBlockId()
block = self.document().findBlockByNumber(blockNumber)
if blockNumber > 0:
prev_block = self.document().findBlockByNumber(blockNumber - 1)
else:
prev_block = block
if blockNumber > 0:
translate_y = -self.verticalScrollBar().sliderPosition()
else:
translate_y = 0
top = self.viewport().geometry().top()
# Adjust text position according to the previous "non entirely visible" block
# if applicable. Also takes in consideration the document's margin offset.
if blockNumber == 0:
# Simply adjust to document's margin
additional_margin = self.document().documentMargin() -1 - self.verticalScrollBar().sliderPosition()
else:
# Getting the height of the visible part of the previous "non entirely visible" block
additional_margin = self.document().documentLayout().blockBoundingRect(prev_block) \
.translated(0, translate_y).intersect(self.viewport().geometry()).height()
# Shift the starting point
top += additional_margin
bottom = top + int(self.document().documentLayout().blockBoundingRect(block).height())
col_1 = QColor(90, 255, 30) # Current line (custom green)
col_0 = QColor(120, 120, 120) # Other lines (custom darkgrey)
# Draw the numbers (displaying the current line number in green)
while block.isValid() and top <= event.rect().bottom():
if block.isVisible() and bottom >= event.rect().top():
number = f"{blockNumber + 1}"
painter.setPen(QColor(120, 120, 120))
if self.textCursor().blockNumber() == blockNumber:
painter.setPen(col_1)
else:
painter.setPen(col_0)
painter.drawText(-5, top,
self.lineNumberArea.width(), self.fontMetrics().height(),
Qt.AlignRight, number)
block = block.next()
top = bottom
bottom = top + int(self.document().documentLayout().blockBoundingRect(block).height())
blockNumber += 1
if __name__ == '__main__':
app = QApplication([])
w = QTextEditHighlighter()
w.show()
app.exec()
LineNumberArea.py
from PySide6.QtCore import QSize
from PySide6.QtWidgets import QWidget
class LineNumberArea(QWidget):
def __init__(self, editor):
QWidget.__init__(self, editor)
self.codeEditor = editor
def sizeHint(self) -> QSize:
return QSize(self.codeEditor.lineNumberAreaWidth(), 0)
def paintEvent(self, event):
self.codeEditor.lineNumberAreaPaintEvent(event)