QLineEdit:设置焦点时光标位置在开头

7

我有一个带有输入掩码的QLineEdit,因此某种类型的代码可以轻松地输入(或粘贴)。由于您可以在QLineEdit中的任何位置放置光标,即使没有文本(因为有来自输入掩码的占位符):

enter image description here

如果人们不够小心和不注意这会导致他们在文本框中间输入,而应该从开头开始输入。我尝试通过安装事件过滤器来确保光标在聚焦时在开头:

bool MyWindowPrivate::eventFilter(QObject * object, QEvent * event)
{
    if (object == ui.tbFoo && event->type() == QEvent::FocusIn) {
        ui.tbFoo->setCursorPosition(0);
    }
    return false;
}

这段代码在键盘聚焦时可以正常工作,例如按下+,但是使用鼠标单击时,光标总是停留在单击的位置。我猜测是QLineEdit在获得焦点后,在单击后自己设置了光标位置,从而撤销了我的位置更改。

深入挖掘一下,单击¹并改变焦点时会引发以下事件:

  1. FocusIn
  2. MouseButtonPress
  3. MouseButtonRelease

我无法准确捕获事件过滤器中的鼠标单击事件,因此有没有一种良好的方法,只在控件获得焦点时(无论是由鼠标还是键盘触发)开始设置光标位置?


¹ 旁注:我讨厌Qt没有关于常见场景信号/事件顺序的文档说明。


事件顺序可能取决于不同的因素(包括用户输入)。因此,不可能描述任何“标准”方式。 - Dmitry Sazonov
关于旁注:我同意。如果你想把它写下来并包含在永久记录中,至少获得文档补丁的批准不应该很难。 - Kuba hasn't forgotten Monica
我无法在事件过滤器中准确捕捉鼠标点击。为什么呢?因为它们被传递为“QMouseEvent”。 - Kuba hasn't forgotten Monica
@Kuba:当然,但是我必须找到一种方式,在鼠标单击之前检查是否有焦点更改,并且以某种方式不会因为某人用键盘更改焦点然后点击控件而出错。这比必要的还要复杂。 - Joey
@Joey:我希望你能发现Thuga的方法 :) - Kuba hasn't forgotten Monica
显示剩余2条评论
3个回答

10
以下是一个被划分为单独类的实现方式。它将设置光标的操作推迟到对象的任何待处理事件之后,从而避免了事件顺序问题。
#include <QApplication>
#include <QLineEdit>
#include <QFormLayout>
#include <QMetaObject>

// Note: A helpful implementation of
// QDebug operator<<(QDebug str, const QEvent * ev)
// is given in https://dev59.com/omEh5IYBdhLWcg3wVCLK

/// Returns a cursor to zero position on a QLineEdit on focus-in.
class ReturnOnFocus : public QObject {
   Q_OBJECT
   /// Catches FocusIn events on the target line edit, and appends a call
   /// to resetCursor at the end of the event queue.
   bool eventFilter(QObject * obj, QEvent * ev) {
      QLineEdit * w = qobject_cast<QLineEdit*>(obj);
      // w is nullptr if the object isn't a QLineEdit
      if (w && ev->type() == QEvent::FocusIn) {
         QMetaObject::invokeMethod(this, "resetCursor",
                                   Qt::QueuedConnection, Q_ARG(QWidget*, w));
      }
      // A base QObject is free to be an event filter itself
      return QObject::eventFilter(obj, ev);
   }
   // Q_INVOKABLE is invokable, but is not a slot
   /// Resets the cursor position of a given widget.
   /// The widget must be a line edit.
   Q_INVOKABLE void resetCursor(QWidget * w) {
      static_cast<QLineEdit*>(w)->setCursorPosition(0);
   }
public:
   ReturnOnFocus(QObject * parent = 0) : QObject(parent) {}
   /// Installs the reset functionality on a given line edit
   void installOn(QLineEdit * ed) { ed->installEventFilter(this); }
};

class Ui : public QWidget {
   QFormLayout m_layout;
   QLineEdit m_maskedLine, m_line;
   ReturnOnFocus m_return;
public:
   Ui() : m_layout(this) {
      m_layout.addRow(&m_maskedLine);
      m_layout.addRow(&m_line);
      m_maskedLine.setInputMask("NNNN-NNNN-NNNN-NNNN");
      m_return.installOn(&m_maskedLine);
   }
};

int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   Ui ui;
   ui.show();
   return a.exec();
}

#include "main.moc"

天哪,我自认为在C++和Qt方面是一个绝对的初学者(到目前为止已经使用了大约1年半左右)。这...对我来说有点难度。我想我现在还是坚持使用子类吧。主要是因为我理解它在做什么(因此我可以相应地记录文档)。不过还是谢谢你提供的解决方案。 - Joey
你真的,真的希望我使用这种方法,对吧? :D 好吧,我屈服了。在复制实现并基本上阅读每一行之后,现在它更有意义了。我仍然害怕自己写。 - Joey
@Joey 我尽力成为一名教育者 :) 只要他们在学习过程中学到了东西(即使是我错了 - 高声望的答案有时也是错误的!),我并不太在意任何人是否使用我的代码。我生活中最大的满足感来自于有人告诉我“哦,我现在明白了”。所以你给了我一点快乐,我感谢你。 - Kuba hasn't forgotten Monica
哦,不客气。公平地说,我在这里不断学习新的东西,虽然很少通过“提问”来学习。通常当我在调试器中看到Qt中的“元”事物时,它是由moc生成的东西,而我已经学会了在心理上跳过那些东西。至少我现在了解了invokeMethodQ_INVOKABLE。关注窗口的问题在另一种变体中也存在,我认为我可以忽略它。我的目标只是为半常见任务提供稍微更好的用户体验 :-) - Joey
让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/50153/discussion-between--and-kuba-ober - Joey
显示剩余2条评论

3
您可以在focusInEvent中使用QTimer来调用一个槽函数将光标位置设置为0。

这样做效果很好,因为单次定时器会在对象的事件队列末尾发布一个定时器事件。由鼠标触发的焦点事件必然已经将鼠标点击事件发布到事件队列中。因此,您可以确保定时器事件(以及随之而来的槽调用)将在任何残留的鼠标按下事件之后被调用。
void LineEdit::focusInEvent(QFocusEvent *e)
{
    QLineEdit::focusInEvent(e);
    QTimer::singleShot(0, this, SLOT(resetCursorPos()));
}

void LineEdit::resetCursorPos()
{
    setCursorPosition(0);
}

哦,我没有考虑到使用零延迟的singleShot直接发布到事件队列中,而设置超时的计时器对我来说感觉很不专业(更不用提光标跳动会让用户感到困惑了)。 - Joey
有趣的是,这在事件过滤器中不起作用,只能在子类中使用。 - Joey
@Joey 如果有一种方法可以将一个函数对象传递给 singleShot,那么它将可以使用事件过滤器。然后它看起来会像这样:QTimer::singleShot(0, this, [this]{ ui->lineEdit.setCursorPosition(0); }); - Kuba hasn't forgotten Monica
@Joey,根据QTimer::singleShot的作用,使用QMetaObject::invokeMethod或者QMetaMethod::invoke并且使用队列连接是完全等效的。这就是我在我的回答中所利用的。请记住:*一个零长度的定时器事件只是一个被放置到事件队列中的QMetaCallEvent*。 - Kuba hasn't forgotten Monica

0

输入掩码的好处是立即使预期模式清晰地呈现给用户。验证器并不会这样做,它只验证内容。这些是两个非常独立的问题。 - Joey
但我认为你把它点踩并不是一个错误的答案。 - neau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接