重写QWidget,实现QLineEdit的基本功能
1、前言如果只是想改样式、限制输入、加按钮、改行为优先继承 QLineEdit。只有在你要完全自绘、自己控制光标、选区、输入法、焦点行为时才建议直接继承 QWidget 重写。因为“用 QWidget 重写一个 QLineEdit”本质上要自己补这几块文本状态管理光标位置和闪烁键盘输入鼠标定位和选区绘制文本、边框、选中态输入法支持剪贴板支持下面给你一个“最小可用版”的单行输入框直接继承 QWidget实现了基础的 QLineEdit 功能。2、核心思路这个控件维护几份状态text_当前文本cursorPos_光标位置selAnchor_选区锚点preedit_输入法预编辑字符串blinkTimer_光标闪烁定时器然后重写这些事件paintEvent自己画边框、文本、选区、光标keyPressEvent处理左右移动、删除、输入、复制粘贴mousePressEvent / mouseMoveEvent鼠标点哪儿光标到哪儿focusInEvent / focusOutEvent控制光标显示和编辑结束inputMethodEvent / inputMethodQuery支持中文输入法3、示例代码头文件#pragma once #include QTimer #include QWidget class MyLineEdit : public QWidget { Q_OBJECT public: enum EchoMode { Normal, Password }; explicit MyLineEdit(QWidget *parent nullptr); QString text() const { return text_; } void setText(const QString text); QString placeholderText() const { return placeholder_; } void setPlaceholderText(const QString text) { placeholder_ text; update(); } EchoMode echoMode() const { return echoMode_; } void setEchoMode(EchoMode mode); int maxLength() const { return maxLength_; } void setMaxLength(int n); QSize sizeHint() const override { return QSize(260, 32); } signals: void textChanged(const QString text); void editingFinished(); void returnPressed(); protected: void paintEvent(QPaintEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void focusInEvent(QFocusEvent *event) override; void focusOutEvent(QFocusEvent *event) override; void inputMethodEvent(QInputMethodEvent *event) override; QVariant inputMethodQuery(Qt::InputMethodQuery query) const override; private: QRect contentRect() const; QString shownText() const; QString shownPreedit() const; int positionFromX(int x) const; void setCursorPos(int pos, bool keepSelection false); bool hasSelection() const { return selAnchor_ ! -1 selAnchor_ ! cursorPos_; } int selectionStart() const { return qMin(selAnchor_, cursorPos_); } int selectionEnd() const { return qMax(selAnchor_, cursorPos_); } void clearSelection() { selAnchor_ -1; } void deleteSelection(); void insertText(const QString s); void resetBlink(); void ensureCursorVisible(); private: QString text_; QString preedit_; QString placeholder_ QStringLiteral(请输入); int cursorPos_ 0; int selAnchor_ -1; int hOffset_ 0; int maxLength_ -1; EchoMode echoMode_ Normal; bool cursorVisible_ true; QTimer blinkTimer_; };实现文件#include MyLineEdit.h #include QClipboard #include QFocusEvent #include QGuiApplication #include QInputMethodEvent #include QKeyEvent #include QMouseEvent #include QPainter MyLineEdit::MyLineEdit(QWidget *parent) : QWidget(parent) { setFocusPolicy(Qt::StrongFocus); setAttribute(Qt::WA_InputMethodEnabled, true); setCursor(Qt::IBeamCursor); setMouseTracking(true); blinkTimer_.setInterval(500); connect(blinkTimer_, QTimer::timeout, this, [this] { cursorVisible_ !cursorVisible_; update(); }); } void MyLineEdit::setText(const QString text) { QString newText text; if (maxLength_ 0 newText.size() maxLength_) { newText newText.left(maxLength_); } if (text_ newText) { return; } text_ newText; preedit_.clear(); cursorPos_ text_.size(); clearSelection(); ensureCursorVisible(); emit textChanged(text_); update(); } void MyLineEdit::setEchoMode(MyLineEdit::EchoMode mode) { if (echoMode_ mode) { return; } echoMode_ mode; ensureCursorVisible(); update(); } void MyLineEdit::setMaxLength(int n) { maxLength_ n; if (maxLength_ 0 text_.size() maxLength_) { text_ text_.left(maxLength_); cursorPos_ qMin(cursorPos_, text_.size()); clearSelection(); emit textChanged(text_); } ensureCursorVisible(); update(); } QRect MyLineEdit::contentRect() const { return rect().adjusted(8, 5, -8, -5); } QString MyLineEdit::shownText() const { if (echoMode_ Password) { return QString(text_.size(), QChar(0x2022)); } return text_; } QString MyLineEdit::shownPreedit() const { if (echoMode_ Password) { return QString(preedit_.size(), QChar(0x2022)); } return preedit_; } int MyLineEdit::positionFromX(int x) const { QFontMetrics fm(font()); QString visual shownText(); int rel x - contentRect().left() hOffset_; if (rel 0) { return 0; } for (int i 0; i visual.size(); i) { int left fm.horizontalAdvance(visual.left(i)); int right fm.horizontalAdvance(visual.left(i 1)); if (rel (left right) / 2) { return i; } } return visual.size(); } void MyLineEdit::setCursorPos(int pos, bool keepSelection) { pos qBound(0, pos, text_.size()); if (keepSelection) { if (selAnchor_ -1) { selAnchor_ cursorPos_; } } else { clearSelection(); } cursorPos_ pos; ensureCursorVisible(); resetBlink(); update(); } void MyLineEdit::deleteSelection() { if (!hasSelection()) { return; } int start selectionStart(); int len selectionEnd() - selectionStart(); text_.remove(start, len); cursorPos_ start; clearSelection(); ensureCursorVisible(); emit textChanged(text_); update(); } void MyLineEdit::insertText(const QString s) { if (s.isEmpty()) { return; } deleteSelection(); QString in s; if (maxLength_ 0) { int remain maxLength_ - text_.size(); if (remain 0) { preedit_.clear(); update(); return; } if (in.size() remain) { in in.left(remain); } } text_.insert(cursorPos_, in); cursorPos_ in.size(); preedit_.clear(); ensureCursorVisible(); emit textChanged(text_); resetBlink(); update(); } void MyLineEdit::resetBlink() { cursorVisible_ true; if (hasFocus()) { blinkTimer_.start(); } } void MyLineEdit::ensureCursorVisible() { QFontMetrics fm(font()); QString visual shownText(); int cursorPixel fm.horizontalAdvance(visual.left(cursorPos_)); int viewWidth contentRect().width(); int margin 6; if (cursorPixel - hOffset_ margin) { hOffset_ qMax(0, cursorPixel - margin); } else if (cursorPixel - hOffset_ viewWidth - margin) { hOffset_ qMax(0, cursorPixel - viewWidth margin); } } void MyLineEdit::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); QRect box rect().adjusted(0, 0, -1, -1); p.setPen(hasFocus() ? QColor(#409EFF) : QColor(#C0C4CC)); p.setBrush(Qt::white); p.drawRoundedRect(box, 4, 4); QRect cr contentRect(); QFontMetrics fm(font()); int baseY cr.y() (cr.height() fm.ascent() - fm.descent()) / 2; p.save(); p.setClipRect(cr); QString visual shownText(); int drawLeft cr.left() - hOffset_; if (visual.isEmpty() preedit_.isEmpty()) { p.setPen(QColor(#A8ABB2)); p.drawText(cr.left(), baseY, placeholder_); } else if (hasSelection() preedit_.isEmpty()) { QString left visual.left(selectionStart()); QString mid visual.mid(selectionStart(), selectionEnd() - selectionStart()); QString right visual.mid(selectionEnd()); int x drawLeft; p.setPen(QColor(#303133)); p.drawText(x, baseY, left); x fm.horizontalAdvance(left); QRect selRect(x, cr.top() 2, fm.horizontalAdvance(mid), cr.height() - 4); p.fillRect(selRect, QColor(#409EFF)); p.setPen(Qt::white); p.drawText(x, baseY, mid); x fm.horizontalAdvance(mid); p.setPen(QColor(#303133)); p.drawText(x, baseY, right); } else { QString display visual; QString preeditDisplay shownPreedit(); if (!preeditDisplay.isEmpty()) { display.insert(cursorPos_, preeditDisplay); } p.setPen(QColor(#303133)); p.drawText(drawLeft, baseY, display); if (!preeditDisplay.isEmpty()) { int preX drawLeft fm.horizontalAdvance(visual.left(cursorPos_)); int preW fm.horizontalAdvance(preeditDisplay); p.setPen(QColor(#409EFF)); p.drawLine(preX, cr.bottom() - 2, preX preW, cr.bottom() - 2); } } if (hasFocus() cursorVisible_) { int x drawLeft fm.horizontalAdvance(visual.left(cursorPos_)); if (!preedit_.isEmpty()) { x fm.horizontalAdvance(shownPreedit()); } p.setPen(QColor(#303133)); p.drawLine(x, cr.top() 3, x, cr.bottom() - 3); } p.restore(); } void MyLineEdit::keyPressEvent(QKeyEvent *event) { QClipboard *cb QGuiApplication::clipboard(); if (event-matches(QKeySequence::Copy)) { if (hasSelection()) { cb-setText(text_.mid(selectionStart(), selectionEnd() - selectionStart())); } return; } if (event-matches(QKeySequence::Cut)) { if (hasSelection()) { cb-setText(text_.mid(selectionStart(), selectionEnd() - selectionStart())); deleteSelection(); } return; } if (event-matches(QKeySequence::Paste)) { insertText(cb-text()); return; } if (event-matches(QKeySequence::SelectAll)) { selAnchor_ 0; cursorPos_ text_.size(); ensureCursorVisible(); update(); return; } bool shift event-modifiers() Qt::ShiftModifier; switch (event-key()) { case Qt::Key_Left: setCursorPos(cursorPos_ - 1, shift); return; case Qt::Key_Right: setCursorPos(cursorPos_ 1, shift); return; case Qt::Key_Home: setCursorPos(0, shift); return; case Qt::Key_End: setCursorPos(text_.size(), shift); return; case Qt::Key_Backspace: if (hasSelection()) { deleteSelection(); } else if (cursorPos_ 0) { text_.remove(cursorPos_ - 1, 1); --cursorPos_; ensureCursorVisible(); emit textChanged(text_); resetBlink(); update(); } return; case Qt::Key_Delete: if (hasSelection()) { deleteSelection(); } else if (cursorPos_ text_.size()) { text_.remove(cursorPos_, 1); ensureCursorVisible(); emit textChanged(text_); resetBlink(); update(); } return; case Qt::Key_Return: case Qt::Key_Enter: emit returnPressed(); emit editingFinished(); return; default: break; } if (!(event-modifiers() (Qt::ControlModifier | Qt::AltModifier))) { QString s event-text(); if (!s.isEmpty() s.at(0).isPrint()) { insertText(s); return; } } QWidget::keyPressEvent(event); } void MyLineEdit::mousePressEvent(QMouseEvent *event) { setFocus(); #if QT_VERSION QT_VERSION_CHECK(6, 0, 0) int x int(event-position().x()); #else int x event-pos().x(); #endif bool keepSelection event-modifiers() Qt::ShiftModifier; setCursorPos(positionFromX(x), keepSelection); QWidget::mousePressEvent(event); } void MyLineEdit::mouseMoveEvent(QMouseEvent *event) { if (event-buttons() Qt::LeftButton) { #if QT_VERSION QT_VERSION_CHECK(6, 0, 0) int x int(event-position().x()); #else int x event-pos().x(); #endif setCursorPos(positionFromX(x), true); } QWidget::mouseMoveEvent(event); } void MyLineEdit::focusInEvent(QFocusEvent *event) { cursorVisible_ true; blinkTimer_.start(); update(); QWidget::focusInEvent(event); } void MyLineEdit::focusOutEvent(QFocusEvent *event) { blinkTimer_.stop(); cursorVisible_ false; preedit_.clear(); clearSelection(); update(); emit editingFinished(); QWidget::focusOutEvent(event); } void MyLineEdit::inputMethodEvent(QInputMethodEvent *event) { if (!event-commitString().isEmpty()) { insertText(event-commitString()); } preedit_ event-preeditString(); ensureCursorVisible(); resetBlink(); update(); } QVariant MyLineEdit::inputMethodQuery(Qt::InputMethodQuery query) const { QFontMetrics fm(font()); int x contentRect().left() - hOffset_ fm.horizontalAdvance(shownText().left(cursorPos_)); switch (query) { case Qt::ImEnabled: return true; case Qt::ImCursorRectangle: return QRect(x, contentRect().top(), 1, contentRect().height()); case Qt::ImFont: return font(); case Qt::ImCursorPosition: return cursorPos_; case Qt::ImAnchorPosition: return hasSelection() ? selAnchor_ : cursorPos_; case Qt::ImSurroundingText: return text_; case Qt::ImCurrentSelection: return hasSelection() ? text_.mid(selectionStart(), selectionEnd() - selectionStart()) : QString(); default: return QVariant(); } }4、实现的功能文本超出宽度后会横向滚动不会直接画出控件外。密码模式会把已输入字符显示成圆点。最大长度会限制键盘输入、粘贴和程序设值。仍然保留了基础选区、复制、剪切、粘贴、全选、输入法支持。它离真正的 QLineEdit 还差什么如果你要做到接近 Qt 自带 QLineEdit下一层通常还要继续补这些撤销和重做右键菜单双击选词拖放文本validatorinputMaskclearButton更完整的输入法行为光标移动按单词跳转style option 和平台风格兼容更接近 QLineEdit 的实现建议如果你想做一个真正长期可维护的自定义输入框不建议把所有逻辑都堆在一个 QWidget 子类里。更稳的结构一般是这 4 层文本模型层负责 text、cursor、selection、preedit、maxLength、echoMode。编辑命令层负责 insert、delete、replace、undo、redo。渲染层负责边框、文本、选区、光标、占位文本、滚动偏移。交互层负责键盘、鼠标、输入法、剪贴板、焦点和快捷键。这样做的好处很直接你后面加 validator 不会把绘制代码弄乱。你后面加 undo/redo 不会把事件处理写炸。你后面加密码模式、只读模式、清空按钮也更好扩展。如果你打算继续往完整控件走建议下一步补这 3 个横向滚动优化把当前的 hOffset 升级成真正的文本视口逻辑支持更平滑的鼠标拖选和长文本操作。编辑命令栈把插入、删除、替换封装成命令顺手就能加 undo/redo。validator 和只读模式这是和业务输入框最相关的能力也是从“演示控件”走向“可用控件”的关键一步。

相关新闻

最新新闻

日新闻

周新闻

月新闻