PDA

View Full Version : QTextBrowser + DOM Access?



Zyl
15th June 2012, 21:44
Hello,

I am trying to create a somewhat versatile chat history message box, and QTextBrowser appears to be the right thing to start with. However, I'd like to be able to manipulate the DOM, the text-color style-attribute specifically. Now I have read QTextBrowser does not support as advanced things as CSS classes, hence provides no sort of DOM-interface. Are there any good alternatives to this? The only way out I see is to keep track of inserted Strings in a seperate data structure and reinsert all the text when a change is required. (Which would work in my case, since colors would only need to change when the user changed a program setting, but is a horribly non-performant thing to do otherwise)

Thanks in advance.

ChrisW67
16th June 2012, 07:55
Extract a pointer to the QTextDocument using QTextEdit::document() and go to town using QTextCursor. See also Rich Text Processing

Or, less directly, maintain your HTML document in a QDomDocument and copy the HTML across to the browser.

Zyl
16th June 2012, 16:33
Thanks for the answer. So you are indeed suggesting to manage the DOM seperately? I tried to do this, but somehow it won't go back in. After changeColor() the textbrowser is empty:


#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->pushButtonInsert, SIGNAL(clicked()), this, SLOT(receiveInput()));
connect(ui->lineEditInput, SIGNAL(returnPressed()), this, SLOT(receiveInput()));
connect(ui->pushButtonSpecial, SIGNAL(clicked()), this, SLOT(changeColor()));
doc = new QDomDocument("channelname.log");
root = doc->createElement("ChatHistory");
doc->appendChild(root);
}

MainWindow::~MainWindow()
{
delete ui;
}

void MainWindow::receiveInput() {
// Get text from input lineEdit
QString message = ui->lineEditInput->text();

// Clear the lineEdit
ui->lineEditInput->clear();

// Create a Message-tag
QDomElement tag = doc->createElement("Message");

// Set attribute sender to dummy client "abc", later to hold actual client names
tag.setAttribute("Sender", "abc");

// Add this tag to QDomElement root, which is already part of the QDomDocument doc
root.appendChild(tag);

// Create a Node containing the text and place it into the Message-tag
QDomText text = doc->createTextNode(message);
tag.appendChild(text);

// Normally append the message to the QTextBrowser, which knows nothing
// about us managing stuff in a seperate QDomDocument
ui->textBrowser->append(message);
}

void MainWindow::changeColor() {
QDomNodeList list = root.childNodes();
ui->textBrowser->clear();
for (int i = 0; i < list.size(); i++) {
QDomElement e = list.at(i).toElement();
if (e.attribute("Sender") == "abc") {
e.setAttribute("style", "color:#0000ff");
}
ui->textBrowser->append(list.at(i).toDocument().toString(-1));
}
}

Any ideas?

wysota
17th June 2012, 22:27
The text browser will certainly NOT understand a full xml document. There is no magic wand here, you need to get your hands dirty.

Zyl
18th June 2012, 02:24
Aye. Is this the kind of dirt you were thinking of?

QString MainWindow::makeTextFromElement(QDomElement e) {
QString tag = e.tagName();
bool addTag = !e.tagName().isEmpty();
QString output = "";
if (addTag) output = "<" + e.tagName();
QDomNamedNodeMap atts = e.attributes();
int len = atts.length();
for (int i = 0; i < len; i++) {
QString name = atts.item(i).toAttr().name();
QString value = atts.item(i).toAttr().value();
output += " " + name + "=\"" + value + "\"";
}
if (addTag) output += ">";
QDomNodeList children = e.childNodes();
len = children.length();
for (int i = 0; i < len; i++) {
output += makeTextFromElement(children.item(i).toElement());
}
output += e.text();
if (addTag) output += "</" + e.tagName() + ">";
return output;
}
(It's working)

wysota
18th June 2012, 13:10
Could be. I guess there are many possible approaches.

Zyl
18th June 2012, 18:33
Still, it'd be preferrable to operate on the Document itself. QTextCursor won't operate on the Html. Selected text is always rich text and never any of the XML elements. That makes it fairly useless.

Here's what I tried:

.h
#ifndef STYLEDCHATTEXTBROWSER_H
#define STYLEDCHATTEXTBROWSER_H

#include <QTextBrowser>
#include <QTextDocument>
#include <QTextCursor>
#include <QMap>
#include <QLinkedList>
#include <QColor>

class StyledChatTextBrowser : public QTextBrowser
{
Q_OBJECT
public:
explicit StyledChatTextBrowser(QWidget *parent = 0);
void logClientMessage(QString clientName, QString message);
void logEventMessage(QString eventType, QString message);
void setColorizeClientNames(bool doColor);
bool getColorizeClientNames();

signals:

public slots:

private:
QTextDocument* doc;
QTextCursor* cursor;
bool doColor;
QMap<QString, QLinkedList<int>* >* clientMessagePositions;

void saveClientColorPosition(QString client, int position);
QColor makeColorFromClientName(QString clientName);

};

#endif // STYLEDCHATTEXTBROWSER_H


.cpp
#include "styledchattextbrowser.h"

#include <QDebug>

StyledChatTextBrowser::StyledChatTextBrowser(QWidg et *parent) :
QTextBrowser(parent)
{
doc = new QTextDocument(this);
doc->clear();
this->setDocument(doc);
cursor = new QTextCursor(doc);
cursor->setPosition(0, QTextCursor::MoveAnchor);
this->doColor = false;
clientMessagePositions = new QMap<QString, QLinkedList<int>* >();

}

void StyledChatTextBrowser::logClientMessage(QString clientName, QString message) {
QString timestamp = "[DUMMYSTAMP]";
QString tagOpen = "<font style=\"color:#000000\">";
QString tagClose = "</font>";
QString line = timestamp + " " + tagOpen + clientName + tagClose + ": " + message + "\n";
int chars = doc->characterCount();
qDebug() << "Chars: " << chars;
cursor->setPosition(chars - 1);
cursor->insertHtml(line);
cursor->setPosition(chars - 1);
QTextCursor searchCursor = doc->find("style=\"color:#", *cursor, QTextDocument::FindCaseSensitively);
int pos = searchCursor.position() + 13;
cursor->setPosition(chars - 1);
saveClientColorPosition(clientName, pos);
qDebug() << doc->toHtml();
}

void StyledChatTextBrowser::logEventMessage(QString eventType, QString message) {

}

void StyledChatTextBrowser::setColorizeClientNames(bool doColor) {
if (doColor != this->doColor) {
QList<QString> clients = clientMessagePositions->keys();
for (int i = 0; i < clientMessagePositions->size(); i++) {
QLinkedList<int>* positions = clientMessagePositions->value(clients.at(i));
QString hexColor = doColor ? makeColorFromClientName(clients.at(i)).name() : "#000000";
QLinkedList<int>::iterator it;
for (it = positions->begin(); it != positions->end(); it++) {
cursor->setPosition(*it + 7, QTextCursor::MoveAnchor);
cursor->movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 7);
cursor->insertText(hexColor);
}
}
}
this->doColor = doColor;
}

bool StyledChatTextBrowser::getColorizeClientNames() {
return doColor;
}

QColor StyledChatTextBrowser::makeColorFromClientName(QSt ring clientName) {
QColor c = QColor();
int r = 70; int g = 120; int b = 170;
for (int i = 0; i < clientName.length(); i++) {
switch (i % 3) {
case 0: r = ((r + b + clientName.at(i).unicode()) % 200) + 50; break;
case 1: g = ((g + b + clientName.at(i).unicode()) % 200) + 50; break;
case 2: b = ((b + r + g + clientName.at(i).unicode()) % 200) + 50; break;
}
}
c.setRed(r);
c.setGreen(g);
c.setBlue(b);
return c;
}

void StyledChatTextBrowser::saveClientColorPosition(QSt ring client, int position) {
QLinkedList<int>* clientMessages = clientMessagePositions->value(client);
if (clientMessages == NULL || clientMessages->empty()) { // Nothing would have been in there if it was empty
clientMessages = new QLinkedList<int>();
clientMessagePositions->insert(client, clientMessages);
}
clientMessages->append(position);
}

Gives output such as:


Starting D:\Projects\Qt\TestTextEdit-build-desktop-Qt_4_7_4_for_Desktop_-_MinGW_4_4__Qt_SDK__Release\release\TestTextEdit.e xe...
Chars: 1
"<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">[DUMMYSTAMP] <span style=" color:#000000;">asd</span>: qwe </p></body></html>"
Chars: 23
"<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">[DUMMYSTAMP]#bb37a2we [DUMMYSTAMP] <span style=" color:#000000;">asd</span>: </p></body></html>"
D:\Projects\Qt\TestTextEdit-build-desktop-Qt_4_7_4_for_Desktop_-_MinGW_4_4__Qt_SDK__Release\release\TestTextEdit.e xe exited with code 0

, when doing logClientMessage("asd", "qwe"), setColorizeClientNames(true), logClientMessage("asd", "").

wysota
18th June 2012, 19:17
I would probably use QTextBlockUserData to store DOM related information in the document. QTextCursor is meant to operate on rich text, that is its purpose. Thus you need to have a mapping between the DOM and QTextDocument. If you add text to the Qt document, you should update the respective DOM node and vice versa.

Zyl
18th June 2012, 20:08
Ah. QTextBlockUserData sounds like the way to go. The QTextBlock deal irritates me though. How is determined how text blocks are formed within the QTextDocument? It says one line = one text block, but you wouldn't call it text block if this wasn't more than just some default setting.

wysota
18th June 2012, 20:29
One block is one paragraph.

Zyl
20th June 2012, 18:44
I got it working better now, but performance is horrible. Changing color of one word in each of a thousand lines takes just a little under one second. Doing the same for 4000 lines takes above 8 seconds. It appears Qt manages the whole document in one large QString. How can I change this behaviour to manage i.e. single text blocks in their own QString?

EDIT: Use of beginEditBlock and endEditBlock helped, but didn't ultimately result in excellent performance. (I have disabled undo/redo on the TextBrowser)

wysota
20th June 2012, 18:47
It appears Qt manages the whole document in one large QString.
No, it doesn't. It manages it as a series of QTextObject instances exactly as is said in the Rich Text Document Structure document in the manual.