I am trying to make a serial terminal program by using QTextBrowser to display incoming data from a serial port. I have set a QTimer to call the paintEvent every 100ms, and show characters on the QTextBrowser widget if anything was received on the serial port.
My problem is that every time I click say in the middle of the QTextBrowser, it is as if though the cursor moves and then on all subsequent ui->tbOutput->insertPlainText(QString(buf));, only half of the QTextBrowser gets updated.
When I click on the bottom of the QTextBrowser widget, the whole QTextBrowser is updated again.
This is the code that I have, where from various other articles, I have tried to scroll to the bottom, and move the text cursor to the end, but it does not do what I want.
void MainWindow::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
static char buf[10240];
if (terminal->serialport.bytesAvailable() > 0)
{
// sizeof(buf)-1 so that there is space for zero termination character
qint64 numread = terminal->serialport.read(buf,sizeof(buf)-1);
if ((numread > 0) && (numread < sizeof(buf)))
{
buf[numread] = 0; // set zero termination
ui->tbOutput->insertPlainText(QString(buf));
ui->tbOutput->verticalScrollBar()->setValue(
ui->tbOutput->verticalScrollBar()->maximum());
ui->tbOutput->textCursor().setPosition(QTextCursor::End);
}
}
}
A few things:
QTextBrowser::textCursor returns a copy, so any modification is not applied to the document
QTextBrowser::setPosition moves the cursor to an absolute position, therefore you are always moving to position 11 (int value to QTextCursor::End). Use QTextBrowser::movePosition instead
finally, it would be better to move the cursor before adding the text, so you are sure it will be added at the end of the document.
Here the modified code:
void MainWindow::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
static char buf[10240];
if (terminal->serialport.bytesAvailable() > 0)
{
// sizeof(buf)-1 so that there is space for zero termination character
qint64 numread = terminal->serialport.read(buf,sizeof(buf)-1);
if ((numread > 0) && (numread < sizeof(buf)))
{
buf[numread] = 0; // set zero termination
auto textCursor = ui->tbOutput->textCursor();
textCursor.movePosition(QTextCursor::End);
ui->tbOutput->setTextCursor(textCursor);
ui->tbOutput->insertPlainText(QString(buf));
ui->tbOutput->verticalScrollBar()->setValue(
ui->tbOutput->verticalScrollBar()->maximum());
}
}
}
On the other hand, some additional considerations:
QIODevice::read(char* data, qint64 maxSize) will read at most maxSize bytes, so checking if the number of read bytes is smaller than your buffer is unnecessary.
Do not do it in the paintEvent, it is not the place to read data but to display it. Instead, connect the timer with a slot and read data there and re-paint your console (ui->tbOutput->update()) only if new data has arrived.
Related
this is my first post here.
I am trying to solve a problem I am having with my SFML project where I am using multiple clients, that communicate through texts that can be typed in the rendered window and then sent to the other sockets using a selector.
My problem is that everytime i press one button of the keyboard, the window detects like 3 or 4, and if I try it on another machine, the behaviour changes.
I tried almost every solution, including the setKeyRepeatEnabled(false);
This is the update function
void Client::Update(Input* input,sf::Event& Ev, sf::Font& font, sf::RenderWindow& window)
{
if (input->isKeyDown(sf::Keyboard::Return))
{
sf::Packet packet;
packet << id + ": " + text;
socket.send(packet);
sf::Text displayText(text, font, 20);
displayText.setFillColor(sf::Color::Red);
chat.push_back(displayText);
text = "";
input->setKeyUp(sf::Keyboard::Return);
}
else if (input->isKeyDown(sf::Keyboard::Backspace))
{
if (text.size() > 0)
text.pop_back();
}
else if (input->isKeyDown(sf::Keyboard::Space))
{
text += ' ';
}
else if (Ev.type == sf::Event::TextEntered)
{
text += Ev.text.unicode;
return;
}
//sf::Event::TextEntered
//text += Ev.text.unicode;
}
This is the render one.
void Client::Render(sf::Font& font, sf::RenderWindow& window)
{
sf::Packet packet;
socket.receive(packet);
std::string temptext;
if (packet >> temptext)
{
sf::Text displayText(temptext, font, 20);
displayText.setFillColor(sf::Color::Blue);
chat.push_back(displayText);
}
int i = 0;
for (i; i < chat.size(); i++)
{
chat[i].setPosition(0, i * 20);
window.draw(chat[i]);
}
sf::Text drawText(text, font, 20);
drawText.setFillColor(sf::Color::Red);
drawText.setPosition(0, i * 20);
window.draw(drawText);
}
I don't recognize the isKeyDown function as part of SFML, so I assume you either is something you implemented or is part of a previous version of SFML (being the current 2.5.1).
There are three ways to detect input from keyboard in SFML.
sf::Keyboard::isKeyPressed: looks like this is the one you are using. It will be true every cycle that the key is pressed. You definetly don't want this. The window.setKeyRepeatEnabled(false) will obviously not work because you don't get the input through the window, but directly from the keyboard.
sf::Event::KeyPressed and sf::Event::KeyReleased events: for this, window.setKeyRepeatEnabled(false) will work, but still it's not the recommended way to deal with text input, as it would require a lot of juggling by your side to handle key combinations (accents, uppa.
sf::Event::TextEntered event: now, this is the best and recommended way to handle typing. Check the tutorial to see how to use it.
Can you trigger the textChanged signal only for certain cases? for example triggering it for inserted text, inserted space characters but not backspace?
I am running checks for typed characters inside a QTextEdit every time the text changes and based on the results highlighting text inside a read-only QTextEdit in the background used as an always-on placeholder text for the user to look at while typing. If the user makes a mistake the character gets highlighted red and reset to it's initial background color after the mistake is fixed. The problem arises when the backspace key is pressed, as it is registered as a mistake and as a result the previous character also gets highlighted red.
void Widget::onTextChanged()
{
QChar c;
QString txt_contents = txtedit_->toPlainText();
if(txt_contents.isEmpty()){
c = '\0';
//reset text display
txtdisplay_->clear();
txtdisplay_->append(*label_text_);
}
else
c = txtedit_->toPlainText().back();
if(!texteditq_->isEmpty()){
if(c == texteditq_->head()){
//refresh text display
correct_++;
txtdisplay_->clear();
txtdisplay_->append(*label_text_);
//remove character that was used for the successful check from the
//queue
texteditq_->dequeue();
}else{
//set backgroud color to red for errors
fmt_->setBackground(Qt::red);
if(!txtedit_->toPlainText().isEmpty()){
//move cursor to the end of the editor, where the error is and save
//the position the error occurs at
c_edit_->movePosition(QTextCursor::End);
quint32 error_pos = c_edit_->position();
//move the cursor in the display for the background text and
//use the KeepAnchor move mode to highlight the misspelled char
c_display_->setPosition(error_pos-1,QTextCursor::MoveAnchor);
c_display_->setPosition(error_pos,QTextCursor::KeepAnchor);
//apply formating to that character
c_display_->setCharFormat(*fmt_);
}
}
}
}
As per OP's request, I am posting a solution that is a class called CustomTextEdit which is subclassing QTextEdit. It is hooking to keyPressEvent() and checking the key that was pressed. If it was other than Backspace then the custom signal keyPressed() will get emitted.
class CustomTextEdit : public QTextEdit {
Q_OBJECT
public:
explicit CustomTextEdit(QWidget *parent = nullptr) : QTextEdit(parent) {}
signals:
void keyPressed();
protected:
void keyPressEvent(QKeyEvent *e) override {
if (e->key() != Qt::Key_Backspace) {
emit keyPressed();
}
QTextEdit::keyPressEvent(e);
}
};
I have a QTextEdit that contains a QTextDocument, which is being programatically edited using the QTextCursor interface. The document is being edited with QTextCursor::insertText().
I load the text file being edited in chunks, so the initial size of the QTextDocument might only be 20 lines even though the document is 100,000 lines. However, I want the QTextEdit scrollbar to reflect the full size of the document instead of just the 20 line document it's currently displaying.
The QTextEdit's scrollbar range is set with QScrollBar::setMaximum() which adjusts the scrollbar to the proper size on the initial opening of the file, but when QTextCursor::insertText() is called the QScrollBar's range is recalculated.
I've already tried calling QScrollBar::setMaximum() after each QTextCursor::insertText() event, but it just makes the whole UI jerky and sloppy.
Is there any way to keep the range of the QScrollBar while the QTextDocument is being modified?
Yes. You'd depend on the implementation detail. In QTextEditPrivate::init(), the following connection is made:
Q_Q(QTextEdit);
control = new QTextEditControl(q);
...
QObject::connect(control, SIGNAL(documentSizeChanged(QSizeF)), q, SLOT(_q_adjustScrollbars()))
Here, q is of the type QTextEdit* and is the Q-pointer to the API object. Thus, you'd need to disconnect this connection, and manage the scroll bars on your own:
bool isBaseOf(const QByteArray &className, const QMetaObject *mo) {
while (mo) {
if (mo->className() == className)
return true;
mo = mo->superClass();
}
return false;
}
bool setScrollbarAdjustmentsEnabled(QTextEdit *ed, bool enable) {
QObject *control = {};
for (auto *ctl : ed->children()) {
if (isBaseOf("QWidgetTextControl", ctl->metaObject()) {
Q_ASSERT(!control);
control = ctl;
}
}
if (!control)
return false;
if (enable)
return QObject::connect(control, SIGNAL(documentSizeChanged(QSizeF)), ed, SLOT(_q_adjustScrollbars()), Qt::UniqueConnection);
else
return QObject::disconnect(control, SIGNAL(documentSizeChanged(QSizeF)), ed, SLOT(_q_adjustScrollbars()));
}
Hopefully, this should be enough to prevent QTextEdit from interfering with you.
I have a QTextEdit with text. The user is allowed to change the text only from the QCursor position stored in startPos variable to the end of document. The begining of the text must remain the same.
I managed to do that by conditioning of QCursor position.
But user can at any moment drag and drop some text in forbidden area.
I want to make a conditional drag and drop according to QCursor position. So, if user drop some text in forbidden area (before cursor position startPos) I want to put that text at the end of the document. And if user drop text after cursor position startPos, user to be allowed to do so.
class BasicOutput : public QTextEdit, public ViewWidgetIFace
{
Q_OBJECT
public:
BasicOutput();
~BasicOutput();
virtual void dragEnterEvent(QDragEnterEvent *e);
virtual void dropEvent(QDropEvent *event);
private:
int startPos;
};
and the rest of simplified (non-functional) code:
BasicOutput::BasicOutput( ) : QTextEdit () {
setInputMethodHints(Qt::ImhNoPredictiveText);
setFocusPolicy(Qt::StrongFocus);
setAcceptRichText(false);
setUndoRedoEnabled(false);
}
void BasicOutput::dragEnterEvent(QDragEnterEvent *e){
e->acceptProposedAction();
}
void BasicOutput::dropEvent(QDropEvent *event){
QPoint p = event->pos(); //get position of drop
QTextCursor t(textCursor()); //create a cursor for QTextEdit
t.setPos(&p); //try convert QPoint to QTextCursor to compare with position stored in startPos variable - ERROR
//if dropCursorPosition < startPos then t = endOfDocument
//if dropCursorPosition >= startPos then t remains the same
p = t.pos(); //convert the manipulated cursor position to QPoint - ERROR
QDropEvent drop(p,event->dropAction(), event->mimeData(), event->mouseButtons(), event->keyboardModifiers(), event->type());
QTextEdit::dropEvent(&drop); // Call the parent function w/ the modified event
}
The errors are:
In member function 'virtual void BasicOutput::dropEvent(QDropEvent*)':
error: 'class QTextCursor' has no member named 'setPos' t.setPos(&p);
error: 'class QTextCursor' has no member named 'pos'p = t.pos();
How to protect the forbidden text area from user drag and drop?
Rspectfully,
Florin.
FINAL CODE
void BasicOutput::dragEnterEvent(QDragEnterEvent *e){
if (e->mimeData()->hasFormat("text/plain"))
e->acceptProposedAction();
else
e->ignore();
}
void BasicOutput::dragMoveEvent (QDragMoveEvent *event){
QTextCursor t = cursorForPosition(event->pos());
if (t.position() >= startPos){
event->acceptProposedAction();
QDragMoveEvent move(event->pos(),event->dropAction(), event->mimeData(), event->mouseButtons(), event->keyboardModifiers(), event->type());
QTextEdit::dragMoveEvent(&move); // Call the parent function (show cursor and keep selection)
}else
event->ignore();
}
You currently have...
QTextCursor t(textCursor()); //create a cursor for QTextEdit
t.setPos(&p);
If you want a QTextCursor associated with the proposed drop location you should use...
QTextCursor t = cursorForPosition(p);
That should fix the first compilation error. Unfortunately there doesn't appear to be any obvious way to get the QPoint associated with a QTextCursor (though there may be a way going via QTextDocument and QTextBlock, I haven't checked). If that's the case then you'll have to perform the drop yourself...
if (t.position() < startPos)
t.movePosition(QTextCursor::End);
setTextCursor(t);
insertPlainText(event->mimeData()->text());
However, can I suggest that what you are attempting to do might prove very confusing to the user. There should be some visual indicator as to what will happen if the text is dropped. How is the user to know that if they drop the text on the forbidden area it will be appended to the end of the current text -- which may not even be visible on a large document?
With that in mind a better approach might be to override dragMoveEvent...
void BasicOutput::dragMoveEvent (QDragMoveEvent *event)
{
QTextCursor t = cursorForPosition(p);
if (t.position() >= startPos)
event->acceptProposedAction();
}
Here the proposed drop action is only accepted if the mouse pointer is not in the forbidden region. Otherwise the user will see (via the pointer glyph or whatever) that the drop will not be accepted.
I don't want mouse middle button to paste text in my QTextEdit. This code doesn't work. TextEdit inherits QTextEdit. After mouse middle button pastes it pastes copied text.
void TextEdit::mousePressEvent ( QMouseEvent * e ) {
if (e->button() == Qt::MidButton) {
e->accept();
return;
};
QTextEdit::mousePressEvent(e);
}
As mouse clicks are usually registered when the button is released, you should redefine the mouseReleaseEvent function.
You don't even need to redefine mousePressEvent, because the middle button isn't handled at all by that function.
I'm assuming you're using Linux here; right clicking in the window is likely to be triggering an insertion of mime data before you get to handle the mouse event, which is why it is still pasting text.
Therefore, according to Qt docs for paste: - " to modify what QTextEdit can paste and how it is being pasted, reimplement the virtual canInsertFromMimeData() and insertFromMimeData() functions."
I've been in the same case, that is to say: having parts of my CustomQTextEdit required to be non-editable.
As I truly love the middle mouse button paste feature, I did not wanted to disable it. So, here is the (more or less quick and dirty coded) workaround I used:
void QTextEditHighlighter::mouseReleaseEvent(QMouseEvent *e)
{
QString prev_text;
if (e->button() == Qt::MidButton) {
// Backup the text as it is before middle button click
prev_text = this->toPlainText();
// And let the paste operation occure...
// e->accept();
// return;
}
// !!!!
QTextEdit::mouseReleaseEvent(e);
// !!!!
if (e->button() == Qt::MidButton) {
/*
* Keep track of the editbale ranges (up to you).
* My way is a single one range inbetween the unique
* tags "//# BEGIN_EDIT" and "//# END_EDIT"...
*/
QRegExp begin_regexp = QRegExp("(^|\n)(\\s)*//# BEGIN_EDIT[^\n]*(?=\n|$)");
QRegExp end_regexp = QRegExp("(^|\n)(\\s)*//# END_EDIT[^\n]*(?=\n|$)");
QTextCursor from = QTextCursor(this->document());
from.movePosition(QTextCursor::Start);
QTextCursor cursor_begin = this->document()->find(begin_regexp, from);
QTextCursor cursor_end = this->document()->find(end_regexp, from);
cursor_begin.movePosition(QTextCursor::EndOfBlock);
cursor_end.movePosition(QTextCursor::StartOfBlock);
int begin_pos = cursor_begin.position();
int end_pos = cursor_end.position();
if (!(cursor_begin.isNull() || cursor_end.isNull())) {
// Deduce the insertion index by finding the position
// of the first character that changed between previous
// text and the current "after-paste" text
int insert_pos; //, end_insert_pos;
std::string s_cur = this->toPlainText().toStdString();
std::string s_prev = prev_text.toStdString();
int i_max = std::min(s_cur.length(), s_prev.length());
for (insert_pos=0; insert_pos < i_max; insert_pos++) {
if (s_cur[insert_pos] != s_prev[insert_pos])
break;
}
// If the insertion point is not in my editable area: just restore the
// text as it was before the paste occured
if (insert_pos < begin_pos+1 || insert_pos > end_pos) {
// Restore text (ghostly)
((MainWindow *)this->topLevelWidget())->disconnect(this, SIGNAL(textChanged()), ((MainWindow *)this->topLevelWidget()), SLOT(on_textEdit_CustomMacro_textChanged()));
this->setText(prev_text);
((MainWindow *)this->topLevelWidget())->connect(this, SIGNAL(textChanged()), ((MainWindow *)this->topLevelWidget()), SLOT(on_textEdit_CustomMacro_textChanged()));
}
}
}
}