QTextDocument and selection painting - c++

I am painting a block of text on a widget with QTextDocument::drawContents. My current goal is to intercept mouse events and emulate text selection. It's pretty clear how to handle the mouse, but displaying the result puzzles me a lot.
Just before we start: I can not use QLabel and let it handle selection on it's own (it has no idea how to draw unusual characters and messes up line height (https://git.macaw.me/blue/squawk/issues/59)), nor I can not use QTextBrowser there - it's just a message bubble, I'm not ready to sacrifice performance there.
There is a very rich framework around QTextDocument, but I can not find any way to make it color the background of some fragment of text that I would consider selected. Found a way to make a frame around a text, found a way to draw under-over-lined text, but it looks like there is simply just no way this framework can draw a background behind text.
I have tried doing this, to see if I can take selected fragment under some QTextFrame and set it's style:
QTextDocument* bodyRenderer = new QTextDocument();
bodyRenderer->setHtml("some text");
bodyRenderer->setTextWidth(50);
painter->setBackgroundMode(Qt::BGMode::OpaqueMode); //this at least makes it color background under all text
QTextFrameFormat format = bodyRenderer->rootFrame()->frameFormat();
format.setBackground(option.palette.brush(QPalette::Active, QPalette::Highlight));
bodyRenderer->rootFrame()->setFrameFormat(format);
bodyRenderer->drawContents(painter);
Nothing of this works too:
QTextBlock b = bodyRenderer->begin();
QTextBlockFormat format = b.blockFormat();
format.setBackground(option.palette.brush(QPalette::Active, QPalette::Highlight));
format.setProperty(QTextFormat::BackgroundBrush, option.palette.brush(QPalette::Active, QPalette::Highlight));
QTextCursor cursor(bodyRenderer);
cursor.setBlockFormat(format);
b = bodyRenderer->begin();
while (b.isValid() > 0) {
QTextLayout* lay = b.layout();
QTextLayout::FormatRange range;
range.format = b.charFormat();
range.start = 0;
range.length = 2;
lay->draw(painter, option.rect.topLeft(), {range});
b = b.next();
}
Is there any way I can make this framework do a simple thing - draw a selection background behind some text? If not - is there a way I can unproject cursor position into coordinate translation, like can I do reverse operation from QAbstractTextDocumentLayout::hitTest just to understand where to draw that selection rectangle myself?

You can use QTextCursor to change the background of the selected text. You only need to select one character at a time to keep the formatting. Here is an example of highlighting in blue (the color of the text is highlighted in white for contrast):
void MainWindow::paintEvent(QPaintEvent *event) {
QPainter painter(this);
painter.fillRect(contentsRect(), QBrush(QColor("white")));
QTextDocument document;
document.setHtml(QString("Hello <font size='20'>world</font> with Qt!"));
int selectionStart = 3;
int selectionEnd = selectionStart + 10;
QTextCursor cursor(&document);
cursor.setPosition(selectionStart);
while (cursor.position() < selectionEnd) {
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); // select one symbol
QTextCharFormat selectFormat = cursor.charFormat();
selectFormat.setBackground(Qt::blue);
selectFormat.setForeground(Qt::white);
cursor.setCharFormat(selectFormat); // set format for selection
cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1);
}
document.drawContents(&painter, contentsRect());
QMainWindow::paintEvent(event);
}

Related

How to appropriately get position of QGraphicsRectItem after drag-release?

I wanted to have an online monitoring system that could tell where the shape is currently, but am getting very weird coordinates of the item, also the dimensions of it get higher by 1 each time I create new one and drag it.
Initial position (map size is 751 by 751, checked by outputting to qDebug(), scene bound to yellow space) :
Dragging it to the left top corner.
As you can see in the beginning it was on (200;200), but after dragging it is on (-201;-196). After deleting it and creating new shape on the same position with the same properties, new shape can't be seen because it is outside of the map, which suggests that edits don't show correct data.
Here is the code of updating the edits:
void CallableGraphicsRectItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
QGraphicsRectItem::mouseReleaseEvent(event);
ptr->updateEdits(this);
}
Here is what I managed to cut down into updateEdits():
void MainWindow::updateEdits(QAbstractGraphicsShapeItem* item)
{
//stuff not related to scene
auto posReal = item->scenePos();
auto pos = posReal.toPoint();
//create QString from coordinates
QString coordinate;
coordinate.setNum(pos.x());
ui->leftXEdit->setText(coordinate);
coordinate.setNum(pos.y());
ui->upperYEdit->setText(coordinate);
//get width and height for rect, radius for circle
auto boundingRectReal = item->sceneBoundingRect();
auto boundingRect = boundingRectReal.toRect();
ui->widthEdit->setText(QString::number(boundingRect.width()));
//disables height edit for circles, not really relevant
if (!items[currentShapeIndex].isRect)
{
ui->heightEdit->setDisabled(true);
}
else
{
ui->heightEdit->setDisabled(false);
ui->heightEdit->setText(QString::number(boundingRect.height()));
}
}
Here is how I anchor the QGraphicsScene to the left top corner of the yellow area:
scene->setSceneRect(0, 0, mapSize.width() - 20, mapSize.height() - 20);
ui->graphicsView->setScene(scene);
How can I report the right data to the edits?
You're better off overriding the itemChange method and using the ItemPositionHasChanged notification. You have to set the ItemSendsGeometryChanges flag on the item so that it receives these notifications.
I'm not sure that your item's final position has been set when you're still in the mouseReleaseEvent method. Tracking it in itemChange will ensure that the data is valid, and this kind of thing is what it's for.
Also, note that "pos" is in the item's parent coordinates, and "boundingRect" is in the item's coordinate space. You should use "scenePos" and "sceneBoundingRect" if you want to be sure you're using scene coordinates. If the item doesn't have a parent, then "pos" and "scenePos" will return the same values, but "boundingRect" and "sceneBoundingRect" will generally differ.

Reimplementing QStyledItemDelegate::paint - How to obtain subelement coordinates?

A QTreeView is rendered with the help of a custom QStyledItemDelegate::paint method. The intention is to add graphical elements to the nodes, e.g. to draw (and fill) a box around the item texts. The tree items may have check boxes, or not.
The Ruby code below achieves the goal, except that I cannot obtain the coordinates of the text element. An empirical offset (x=29; y=4) serves as a workaround. The super method draws the text on top of the box.
How can I obtain the coordinates of the text element?
Is this the right approach at all, or do I have to use drawText and drawControl instead of calling the superclass paint method? In that case, how do you control the layout of the sub elements?
(This question is not Ruby specific. Answers containing C++ are welcome.)
class ItemDelegate < Qt::StyledItemDelegate
def paint(painter, option, index)
text = index.data.toString
bg_color = Qt::Color.new(Qt::yellow)
fg_color = Qt::Color.new(Qt::black)
offset = Qt::Point.new(29,4)
painter.save
painter.translate(option.rect.topLeft + offset)
recti = Qt::Rect.new(0, 0, option.rect.width, option.rect.height)
rectf = Qt::RectF.new(recti)
margin = 4
bounding = painter.boundingRect(rectf, Qt::AlignLeft, text)
tbox = Qt::RectF.new(Qt::PointF.new(-margin,0), bounding.size)
tbox.width += 2*margin
painter.fillRect(tbox, bg_color)
painter.drawRect(tbox)
painter.restore
super
end
end
Edit: Please find a self-contained example here in this Gist.
I had the same problem in C++. Unfortunately, the workaround on option.rect.* properties seems to be the only way to find the text coords.
Here the paint method of my delegate:
void ThumbnailDelegate::paint(QPainter *p_painter, const QStyleOptionViewItem &p_option, const QModelIndex &p_index) const
{
if(p_index.isValid())
{
const QAbstractItemModel* l_model = p_index.model();
QPen l_text_pen(Qt::darkGray);
QBrush l_brush(Qt::black, Qt::SolidPattern);
/** background rect **/
QPen l_pen;
l_pen.setStyle(Qt::SolidLine);
l_pen.setWidth(4);
l_pen.setBrush(Qt::lightGray);
l_pen.setCapStyle(Qt::RoundCap);
l_pen.setJoinStyle(Qt::RoundJoin);
p_painter->setPen(l_pen);
QRect l_border_rect;
l_border_rect.setX(p_option.rect.x() + 5);
l_border_rect.setY(p_option.rect.y() + 5);
l_border_rect.setWidth(p_option.rect.width() - 16);
l_border_rect.setHeight(p_option.rect.height() - 16);
QPainterPath l_rounded_rect;
l_rounded_rect.addRect(QRectF(l_border_rect));
p_painter->setClipPath(l_rounded_rect);
/** background color for hovered items **/
p_painter->fillPath(l_rounded_rect, l_brush);
p_painter->drawPath(l_rounded_rect);
/** image **/
QPixmap l_pixmap = bytearrayToPixmap(l_model->data(p_index, ImageRole).toByteArray()).scaled(150, 150, Qt::KeepAspectRatio);
QRect l_img_rect = l_border_rect;
int l_img_x = (l_img_rect.width()/2 - l_pixmap.width()/2)+l_img_rect.x();
l_img_rect.setX(l_img_x);
l_img_rect.setY(l_img_rect.y() + 12);
l_img_rect.setWidth(l_pixmap.width());
l_img_rect.setHeight(l_pixmap.height());
p_painter->drawPixmap(l_img_rect, l_pixmap);
/** label **/
QRect l_txt_rect = p_option.rect;
l_txt_rect.setX(l_border_rect.x()+5);
l_txt_rect.setY(l_border_rect.y() + l_border_rect.height() -20);
l_txt_rect.setHeight(20);
l_txt_rect.setWidth(l_txt_rect.width()-20);
QFont l_font;
l_font.setBold(true);
l_font.setPixelSize(12);
p_painter->setFont(l_font);
p_painter->setPen(l_text_pen);
QString l_text = l_model->data(p_index, TextRole).toString();
p_painter->drawText(l_txt_rect, Qt::ElideRight|Qt::AlignHCenter, l_text);
}
else
{
qWarning() << "ThumbnailDelegate::paint() Invalid index!";
}
}
I am not skilled on Ruby but, as you can see, I am using drawPath, drawPixmap and drawText.
Here is the result:
I think it is better to avoid invoking paint from the superclass, since it should be done automatically by Qt and you may break something on the UI lifecycle.

Qt Code editor display the line number in an area to the RIGHT

I am just learning Qt. I want to show line number of QPlainTextEdit. I found this link
and it worked. But now I want the editor displays the line numbers in an area to the RIGHTof the area for editing. I have been searching google very much, but I can't solve. How to solve?
In addition to GPPK's answer, you also need to change the viewport margins:
void CodeEditor::updateLineNumberAreaWidth(int /* newBlockCount */)
{
setViewportMargins(0, 0, lineNumberAreaWidth(), 0);
}
GPPK's code assigns the correct drawing rectangle to the sub-widget, my code makes sure, that the scrollview does not paint into that area.
In your link it shows you how it draws the line number area on the left:
void CodeEditor::resizeEvent(QResizeEvent *e)
{
QPlainTextEdit::resizeEvent(e);
QRect cr = contentsRect();
lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
}
In order to draw the line numbers from the right you will (this is untested) do something like this:
void CodeEditor::resizeEvent(QResizeEvent *e)
{
QPlainTextEdit::resizeEvent(e);
QRect cr = contentsRect();
lineNumberArea->setGeometry(QRect(cr.right() - lineNumberArea->width() , cr.top(), lineNumberAreaWidth(), cr.height()));
}

highlight a word that has x and y as a position inside a qtextedit

I am writing a code editor using Qt. I have a problem, I am wondering how to highlight a word inside my textedit using it's x and y position (line and column) , any idea please?
Using this method i can get the position and highlight some characters, but after that if i want to display an other text all the text was been highlighted.
void MainWindow::Target(int row, int colum)
{
QTextEdit* TempTextEdit = ui->textEdit;
QTextDocument* document = TempTextEdit->document();
QTextCursor cursor(document);
//cursor.beginEditBlock();
// cursor.setPosition(0);
cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, row-1);
cursor.setPosition(colum);
cursor.setPosition(colum+2, QTextCursor::KeepAnchor);
QTextCharFormat plainFormat(cursor.charFormat());
QTextCharFormat colorFormat = plainFormat;
colorFormat.setForeground(Qt::red);
colorFormat.setFontUnderline(true);
cursor.movePosition(QTextCursor::WordRight, QTextCursor::KeepAnchor);
cursor.mergeCharFormat(colorFormat);
// cursor.endEditBlock();
TempTextEdit->setTextCursor(cursor);
}
You can use QSyntaxHighlighter.

Usage of CScrollView in MFC application

I am using CSCrollView window for our Application in which i have table drawn in View.
I have derived the CMYclass from CSCrollView, But whenevr i am scrolling the window up and down whatever i have drwan is getting erased. How i can acheive it this... i need to perform same actvity like a Word Pad is doing with images and Text. I want to keep scroll the View Vertically. Till the page ends.
Here is the code snippet:-
void CMyView::OnInitialUpdate()
{
CSize sizeTotal;
// TODO: calculate the total size of this view
sizeTotal.cx = 450;
sizeTotal.cy = 700;
SetScrollSizes(MM_TEXT, sizeTotal);
}
void CMyView::OnDraw(CDC* pDC)
{
for(int i = 1;i<50;++i)
{
AddRow(pDC);
TopPos = Height+TopPos;// ![Outpu Window Image][1]
nCountRow++;
}
}
it is only drawing 18 rows, but when iam scrolling down above drawn content is no more and also there is nothing coming up in scrolled area.
Is there anything more need to add?
Thanking for help
Regards,
Mukesh
Long time I have used CScrollView. Here is my hint: Try mapping modes other than MM_TEXT. Also look up other functions in CScrollView. I suggest to draw simple stuff first than some complicated rows.