Qt(通过 Common Lisp 的 qtools):QLineEdit 在点击时不激活,但在 TAB 上获得焦点

Qt (via Common Lisp's qtools): QLineEdit does not activate on click but receives focus on TAB

问题

我运行遇到了一个相当奇怪的问题:界面中的QLineEdit object无法通过鼠标点击访问,但使用Tab的他们接收焦点并响应键盘输入。但即使他们有焦点,他们也不会响应点击。更奇怪的是,最后添加的 QLineEdit 确实对点击有反应。

额外问题:mouse-inaccessible line-edits 不显示 tool-tips.

我正在使用 Common Lisp 的 qtools 构建接口,因此我将不得不做一些解释它是如何工作的。简而言之,我在循环中添加 line-edits one-by-one,所有这些都以相同的方式创建并添加到 QGridLayout.

已更新:请参阅下文了解问题的可能来源

参数小部件

参数控件用于表示一个参数输入。每个参数都有一个名称、值和(度量)单位。名称和单位显示为标签,值是可编辑的并由 QLineEdit

表示
(define-widget parameter-widget (QWidget)
  ((parameter :initarg :parameter)))

(define-subwidget (parameter-widget label) (q+:make-qlabel parameter-widget)
  (setf (q+:text label) (parameter-base-name parameter)))

(define-subwidget (parameter-widget entry) (q+:make-qlineedit parameter-widget)
  (setf (q+:alignment entry) +align-right+)
  (setf (q+:text entry) (format nil "~A" (parameter-value parameter)))
  (setf (q+:tool-tip entry) (parameter-base-description parameter)))

(define-subwidget (parameter-widget units) (q+:make-qlabel parameter-widget)
  (setf (q+:text units) (parameter-units parameter)))

为了表明参数值已经改变,parameter-widget 将发出新的信号 parameter-changed(它被转换为 Qt 的信号 "parameterChanged"):

(define-signal (parameter-widget parameter-changed) ())

行编辑的插槽尝试将文本转换为数字,如果成功,则更新基础参数并发出 parameter-changed:

(define-slot (parameter-widget parameter-changed) ((new-text string))
  (declare (connected entry (text-changed string)))
  (handler-case
      (let ((new-number (parse-number new-text)))
        (setf (parameter-value parameter) new-number)
        (signal! parameter-widget (parameter-changed)))
    (error nil)))

这个方法只是自动构造parameter类型的object的widget(工厂方法):

(defmethod make-parameter-ui ((object parameter))
  (make-instance 'parameter-widget :parameter object))

parameter-container(请参见further-below)中的多个单个参数需要此方法来对齐标签:

(defmethod add-to-grid ((widget parameter-widget) grid row column)
  (with-slots (label entry units) widget
    (q+:add-widget grid label row column 1 1)
    (q+:add-widget grid entry row (1+ column) 1 1)
    (q+:add-widget grid units row (+ column 2) 1 1)))

参数容器

因此,如果我单独使用 parameter-widget - 一切正常。但大多数时候我需要 parameter-container:

形式的多个参数
(define-widget parameter-container-widget (QWidget)
  ((parameter-container :initarg :parameter-container)))

此插槽捕获来自所有 children parameter 的信号 parameter-changed 和 re-emits 容器

(define-slot (parameter-container-widget parameter-changed) ()
  (format t "~&Re-emitting the signal...~%")
  (signal! parameter-container-widget (parameter-changed)))

layoutQGridLayoutparameter-container 的所有 parameter-children 放置所有个体 parameter-widget。它所做的一切:

代码:

(define-subwidget (parameter-container-widget layout)
    (q+:make-qgridlayout parameter-container-widget)
  (loop for p in (parameter-container-children parameter-container)
     for row from 0
     do (let ((parameter-widget (make-parameter-ui p)))
          (setf (q+:parent parameter-widget) parameter-container-widget)
          (add-to-grid parameter-widget layout row 0)
          (connect! parameter-widget (parameter-changed)
                    parameter-container-widget
                    (parameter-changed)))))

此方法统一处理parameter-widgetparameter-container:

(defmethod add-to-grid ((widget parameter-container-widget) grid row column)
  (q+:add-widget grid widget row column 1 1))

和泛型实例化器(工厂方法):

(defmethod make-parameter-ui ((object parameter-container))
  (make-instance 'parameter-container-widget :parameter-container object))

系统信息

Qt4.8,Common Lisp:SBCL 1.3.7,来自 Quicklisp 的 qtools,OS:Ubuntu16.04

更新

Windows10

同样的问题

更新 2:完整的最小示例

抱歉,上面的代码使用了其他包中的许多定义:全部放上去会很长。最小的例子归结为:

(ql:quickload '(:qtools :qtcore :qtgui))

(defpackage :qtexample
  (:use #:cl+qt))

(in-package qtexample)

(in-readtable :qtools)


(defclass parameter ()
  ((name :initarg :name :accessor parameter-name)
   (value :initarg :value :accessor parameter-value)
   (units :initarg :units :accessor parameter-units)))

(defun parameter (name value units)
  (make-instance 'parameter
    :name name
    :value value
    :units units))

(defvar *mass* (parameter "mass" 1d0 "kg"))
(defvar *velocity* (parameter "velocity" 0.5d0 "m/s"))

(defvar *temperature* (parameter "temperature" 300d0 "K"))
(defvar *pressure* (parameter "pressure" 1d5 "Pa"))

(define-widget parameter-widget (QWidget)
  ((parameter
    :initarg :parameter
    :accessor parameter-widget-parameter)))

(define-subwidget (parameter-widget label)
    (q+:make-qlabel parameter-widget)
  (setf (q+:text label) (parameter-name parameter)))

(defmethod make-ui ((object parameter))
  (make-instance 'parameter-widget :parameter object))

(defconstant +align-right+ 2)

(define-subwidget (parameter-widget entry)
    (q+:make-qlineedit parameter-widget)
  (setf (q+:alignment entry) +align-right+)
  (setf (q+:text entry) (format nil "~A" (parameter-value parameter))))

(define-subwidget (parameter-widget units)
    (q+:make-qlabel parameter-widget)
  (setf (q+:text units) (parameter-units parameter)))

(define-signal (parameter-widget parameter-changed) ())

(define-slot (parameter-widget entry) ((new-text string))
  (declare (connected entry (text-changed string)))
  (format t "~&Parameter has changed~%")
  (handler-case
      (let ((new-number (parse-number:parse-number new-text)))
        (setf (parameter-value parameter) new-number)
        (signal! parameter-widget (parameter-changed)))
    (error nil)))

(defmethod add-to-grid ((widget parameter-widget) grid row column)
  (with-slots (label entry units) widget
    (q+:add-widget grid label row column 1 1)
    (q+:add-widget grid entry row (1+ column) 1 1)
    (q+:add-widget grid units row (+ column 2) 1 1)
    (list (1+ row) (+ column 3))))

(define-widget parameter-widget-window (QWidget)
  ((parameter :initarg :parameter)))


(define-subwidget (parameter-widget-window parameter-widget)
    (make-ui parameter))

(define-subwidget (parameter-widget-window grid)
    (q+:make-qgridlayout  parameter-widget-window)
  (add-to-grid parameter-widget grid 0 0))

(defun parameter-example (parameter)
  (with-main-window
      (window (make-instance 'parameter-widget-window
                :parameter parameter))))

(define-widget parameter-container-widget (QWidget)
  ((parameter-container
    :initarg :parameter-container
    :accessor parameter-container-widget-parameter-container)))

(defmethod make-ui ((object list))
  (make-instance 'parameter-container-widget :parameter-container object))

(define-slot (parameter-container-widget parameter-changed) ()
  (format t "~&Re-emitting the signal...~%")
  (signal! parameter-container-widget (parameter-changed)))

(define-subwidget (parameter-container-widget layout)
    (q+:make-qgridlayout parameter-container-widget)
  (let* ((parameter-widgets (loop for p in parameter-container
                               collect (make-ui p))))
    (loop for p in parameter-widgets
       for row from 0
       do (progn
            (setf (q+:parent p) parameter-container-widget)
            (add-to-grid p layout row 0)
            (connect! p
                      (parameter-changed)
                      parameter-container-widget
                      (parameter-changed))))))

(define-widget parameter-container-widget-window (QWidget)
  ((parameter-container :initarg :parameter-container)))

(define-subwidget (parameter-container-widget-window container-widget)
    (make-ui parameter-container)
  (setf (q+:parent container-widget) parameter-container-widget-window))

(define-slot (parameter-container-widget-window parameter-changed) ()
  (declare (connected container-widget (parameter-changed)))
  (format t "~&Got parameter changed~%"))


(defmethod add-to-grid ((widget parameter-container-widget) grid row column)
  (q+:add-widget grid widget row column))

(defun example-parameter-container (parameter-container)
  (with-main-window
      (window (make-instance 'parameter-container-widget-window
                :parameter-container parameter-container))))

;; to run:
(example-parameter-container (list *mass* *velocity*))

在处理它时,我偶然发现了一个可能的解决方案。这一行

(setf (q+:parent p) parameter-container-widget) 

将 sub-widget p(在 sub-widget 的列表中)的 parent 设置为 parameter-container-widget。如果注释掉这一行,一切正常。

p 的 sub-widget(包括 entryQLineEdit 的实例)后来被添加到网格中,但不包括 p本身!在某种程度上,parameter-widget 不是一个合适的小部件:它只是其他小部件的集合,具有如何将它们添加到容器中的规则。但它需要充当能够接收和发送信号的小部件。

根据@KubaOber 和@Shinmera 的建议,我设法解决了这个问题。我在这里发布修复以供将来参考。

预选赛没有改变(除了,我忘了添加:PARSE-NUMBER系统):

(ql:quickload '(:qtools :qtcore :qtgui :parse-number))

(defpackage :qtexample
  (:use #:cl+qt))

(in-package qtexample)

(in-readtable :qtools)


(defclass parameter ()
  ((name :initarg :name :accessor parameter-name)
   (value :initarg :value :accessor parameter-value)
   (units :initarg :units :accessor parameter-units)))

(defun parameter (name value units)
  (make-instance 'parameter
    :name name
    :value value
    :units units))

(defvar *mass* (parameter "mass" 1d0 "kg"))
(defvar *velocity* (parameter "velocity" 0.5d0 "m/s"))

(defvar *temperature* (parameter "temperature" 300d0 "K"))
(defvar *pressure* (parameter "pressure" 1d5 "Pa"))

因为 PARAMETER-WIDGET 不是真正的小部件,而只是其他小部件的容器(并且它本身不能添加到适当的小部件容器中 - 否则标签的对齐方式会消失 - 参见ADD-TO-GRID方法)但它需要能够接收和发送信号,它继承自QObject而不是QWidget。现在所有的子部件都是正常的 class-slots 而不是由 (DEFINE-SUBWIDGET ...) 定义。请注意,没有为任何子小部件提供父级:当这些小部件添加到容器小部件时,将分配父级 属性。

(define-widget parameter-widget (QObject)
  ((parameter
    :initarg :parameter
    :accessor parameter-widget-parameter)
   (label :initform (q+:make-qlabel))
   (entry :initform (q+:make-qlineedit))
   (units :initform (q+:make-qlabel))))

初始化进入INITIALIZE-INSTANCE :AFTER方法:

(defconstant +align-right+ 2)

(defmethod initialize-instance :after ((object parameter-widget) &key)
  (with-slots (parameter label entry units) object
    (setf (q+:text label) (parameter-name parameter))
    (setf (q+:text entry) (format nil "~A" (parameter-value parameter)))
    (setf (q+:alignment entry) +align-right+)
    (setf (q+:text units) (parameter-units parameter))))

其余定义保持不变(信号大部分与问题无关,但有助于理解我为什么要跳过所有这些箍):

(defmethod make-ui ((object parameter))
  (make-instance 'parameter-widget :parameter object))

(define-signal (parameter-widget parameter-changed) ())

(define-slot (parameter-widget entry) ((new-text string))
  (declare (connected entry (text-changed string)))
  (format t "~&Parameter has changed~%")
  (handler-case
      (let ((new-number (parse-number:parse-number new-text)))
        (setf (parameter-value parameter) new-number)
        (signal! parameter-widget (parameter-changed)))
    (error nil)))

(defmethod add-to-grid ((widget parameter-widget) grid row column)
  (with-slots (label entry units) widget
    (q+:add-widget grid label row column 1 1)
    (q+:add-widget grid entry row (1+ column) 1 1)
    (q+:add-widget grid units row (+ column 2) 1 1)
    (list (1+ row) (+ column 3))))

这是一个快速演示,一切都适用于单个参数。现在,PARAMETER-WIDGET 不再是一个小部件,因此它被添加为一个 class-slot。

(define-widget parameter-widget-window (QWidget)
  ((parameter :initarg :parameter)
   (parameter-widget)))

window的网格:

(define-subwidget (parameter-widget-window grid)
    (q+:make-qgridlayout  parameter-widget-window))

此处将 PARAMETER-WIDGET 组件添加到网格中已移出此定义。原因:slot PARAMETER-WIDGET 此时未绑定。它确实在 INITIALIZE-INSTANCE :AFTER 方法中绑定并且所有组件都添加到网格中:

(defmethod initialize-instance :after ((object parameter-widget-window) &key)
  (with-slots (parameter parameter-widget grid) object
    (setf parameter-widget (make-ui parameter))
    (setf (q+:parent parameter-widget) object)
    (add-to-grid parameter-widget grid 0 0)))

启动器保持不变:

(defun parameter-example (parameter)
  (with-main-window
      (window (make-instance 'parameter-widget-window
                :parameter parameter))))

至运行:(parameter-example *mass*)

PARAMETER-CONTAINER-WIDGET 是一个合适的小部件。它的定义没有改变:

(define-widget parameter-container-widget (QWidget)
  ((parameter-container
    :initarg :parameter-container
    :accessor parameter-container-widget-parameter-container)))

(defmethod make-ui ((object list))
  (make-instance 'parameter-container-widget :parameter-container object))

(define-slot (parameter-container-widget parameter-changed) ()
  (format t "~&Re-emitting the signal...~%")
  (signal! parameter-container-widget (parameter-changed)))

LAYOUT定义也没有改变。但是现在可以安全地将 p (PARAMETER-WIDGET) 的父级 属性 设置为 PARAMETER-WIDGET-CONTAINER,因此当容器被销毁时它也会被销毁。

(define-subwidget (parameter-container-widget layout)
    (q+:make-qgridlayout parameter-container-widget)
  (let* ((parameter-widgets (loop for p in parameter-container
                               collect (make-ui p))))
    (loop for p in parameter-widgets
       for row from 0
       do (progn
            (add-to-grid p layout row 0)
            (let ((pp p))
              (setf (q+:parent pp) parameter-container-widget))
            (connect! p
                      (parameter-changed)
                      parameter-container-widget
                      (parameter-changed))))))

将此小部件添加到网格是微不足道的(但不完全正确,但可以修复,请参见下文):

(defmethod add-to-grid ((widget parameter-container-widget) grid row column)
  (q+:add-widget grid widget row column))

演示部分没有变化:

(define-widget parameter-container-widget-window (QWidget)
  ((parameter-container :initarg :parameter-container)))

(define-subwidget (parameter-container-widget-window container-widget)
    (make-ui parameter-container)
  (setf (q+:parent container-widget) parameter-container-widget-window))

(define-slot (parameter-container-widget-window parameter-changed) ()
  (declare (connected container-widget (parameter-changed)))
  (format t "~&Got parameter changed~%"))

(defun example-parameter-container (parameter-container)
  (with-main-window
      (window (make-instance 'parameter-container-widget-window
                :parameter-container parameter-container))))

至运行:(example-parameter-container (list *mass* *velociy*))

它也可以像这样处理分层参数

(example-parameter-container (list *mass* *velocity* (list *temperature* *pressure*))) 

而这个 运行 将说明为什么 PARAMETER-CONTAINER-WIDGETADD-TO-GRID 需要更复杂一点,但这是一个不同的故事。