目的
通常我们都会应用到拖动某个小部件, 然后获取部件的位置pos, 发送信号或者以及相关需求处理。因为窗口,控件的移动,拖动导致pos()变化,从而引起定义的属性变化设置,我们常用此来自定义部件组合成更复杂的控件. 这篇博客会以doubleslider拖动句柄为例。
位置
通常一个子控件的位置pos是以父窗口中左上角为原点。
具体以MainWindow 中拖入一个 pushButton为例, 它的geometry 属性表明此时是以在父窗口MainWindow位置是70,90 长宽为231,71。
当我再次拖入一个控件时,pushButton_2是放在widget 里面, 在MainWindow这两个位置是除了y之外其他的都是一样的, 但是会发现pushButton_2 geometry 属性不同, 它是相对于widget 的, 因此我们在对于复杂的空间结构时, 要注意移动的控件位置移动相对场景。这也是为啥有QWidget::mapToGlobal, QWidget::mapFromGlobal, QWidget::mapFromParent, QWidget::mapToParent…这些函数原因,使得控件Point能够转换在不同场景(parent, global, screen)坐标系下坐标或者位置。可参考博客
UI控件借鉴设计
思路: 这是一个双轴的滑动条, 通过这个达到某种需求(以实际需求为准)。设计有个难点:如何保证拖动过程中对应数据准确性, 同时在窗口涉及size 变化过程中保证原有数据不变化性。
它不同于Qt自带的QSlider,QSlider 原生控件样子如下
参考源码发现,它是画上去的,参考qt 5.12.1
void paintEvent(QPaintEvent *ev) override;
void QSlider::paintEvent(QPaintEvent *)
{
Q_D(QSlider);
QPainter p(this);
QStyleOptionSlider opt;
initStyleOption(&opt);
opt.subControls = QStyle::SC_SliderGroove | QStyle::SC_SliderHandle;
if (d->tickPosition != NoTicks)
opt.subControls |= QStyle::SC_SliderTickmarks;
if (d->pressedControl) {
opt.activeSubControls = d->pressedControl;
opt.state |= QStyle::State_Sunken;
} else {
opt.activeSubControls = d->hoverControl;
}
style()->drawComplexControl(QStyle::CC_Slider, &opt, &p, this);
}
void mousePressEvent(QMouseEvent *ev) override;
void mouseReleaseEvent(QMouseEvent *ev) override;
void mouseMoveEvent(QMouseEvent *ev) override;
void QSlider::mousePressEvent(QMouseEvent *ev)
{
Q_D(QSlider);
if (d->maximum == d->minimum || (ev->buttons() ^ ev->button())) {
ev->ignore();
return;
}
#ifdef QT_KEYPAD_NAVIGATION
if (QApplication::keypadNavigationEnabled())
setEditFocus(true);
#endif
ev->accept();
if ((ev->button() & style()->styleHint(QStyle::SH_Slider_AbsoluteSetButtons)) == ev->button()) {
QStyleOptionSlider opt;
initStyleOption(&opt);
const QRect sliderRect = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);
const QPoint center = sliderRect.center() - sliderRect.topLeft();
// to take half of the slider off for the setSliderPosition call we use the center - topLeft
setSliderPosition(d->pixelPosToRangeValue(d->pick(ev->pos() - center)));
triggerAction(SliderMove);
setRepeatAction(SliderNoAction);
d->pressedControl = QStyle::SC_SliderHandle;
update();
} else if ((ev->button() & style()->styleHint(QStyle::SH_Slider_PageSetButtons)) == ev->button()) {
QStyleOptionSlider opt;
initStyleOption(&opt);
d->pressedControl = style()->hitTestComplexControl(QStyle::CC_Slider,
&opt, ev->pos(), this);
SliderAction action = SliderNoAction;
if (d->pressedControl == QStyle::SC_SliderGroove) {
const QRect sliderRect = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);
int pressValue = d->pixelPosToRangeValue(d->pick(ev->pos() - sliderRect.center() + sliderRect.topLeft()));
d->pressValue = pressValue;
if (pressValue > d->value)
action = SliderPageStepAdd;
else if (pressValue < d->value)
action = SliderPageStepSub;
if (action) {
triggerAction(action);
setRepeatAction(action);
}
}
} else {
ev->ignore();
return;
}
if (d->pressedControl == QStyle::SC_SliderHandle) {
QStyleOptionSlider opt;
initStyleOption(&opt);
setRepeatAction(SliderNoAction);
QRect sr = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);
d->clickOffset = d->pick(ev->pos() - sr.topLeft());
update(sr);
setSliderDown(true);
}
}
/*!
\reimp
*/
void QSlider::mouseMoveEvent(QMouseEvent *ev)
{
Q_D(QSlider);
if (d->pressedControl != QStyle::SC_SliderHandle) {
ev->ignore();
return;
}
ev->accept();
int newPosition = d->pixelPosToRangeValue(d->pick(ev->pos()) - d->clickOffset);
QStyleOptionSlider opt;
initStyleOption(&opt);
setSliderPosition(newPosition);
}
/*!
\reimp
*/
void QSlider::mouseReleaseEvent(QMouseEvent *ev)
{
Q_D(QSlider);
if (d->pressedControl == QStyle::SC_None || ev->buttons()) {
ev->ignore();
return;
}
ev->accept();
QStyle::SubControl oldPressed = QStyle::SubControl(d->pressedControl);
d->pressedControl = QStyle::SC_None;
setRepeatAction(SliderNoAction);
if (oldPressed == QStyle::SC_SliderHandle)
setSliderDown(false);
QStyleOptionSlider opt;
initStyleOption(&opt);
opt.subControls = oldPressed;
update(style()->subControlRect(QStyle::CC_Slider, &opt, oldPressed, this));
}
pixelPosToRangeValue
int QSliderPrivate::pixelPosToRangeValue(int pos) const
{
Q_Q(const QSlider);
QStyleOptionSlider opt;
q->initStyleOption(&opt);
QRect gr = q->style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, q);
QRect sr = q->style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, q);
int sliderMin, sliderMax, sliderLength;
if (orientation == Qt::Horizontal) {
sliderLength = sr.width();
sliderMin = gr.x();
sliderMax = gr.right() - sliderLength + 1;
} else {
sliderLength = sr.height();
sliderMin = gr.y();
sliderMax = gr.bottom() - sliderLength + 1;
}
return QStyle::sliderValueFromPosition(minimum, maximum, pos - sliderMin,
sliderMax - sliderMin, opt.upsideDown);
}
核心关键 将value 与 position 转换 参考源码 sliderValueFromPosition, sliderPositionFromValue 设计
int QStyle::sliderValueFromPosition(int min, int max, int pos, int span, bool upsideDown)
{
if (span <= 0 || pos <= 0)
return upsideDown ? max : min;
if (pos >= span)
return upsideDown ? min : max;
uint range = max - min;
if ((uint)span > range) {
int tmp = (2 * pos * range + span) / (2 * span);
return upsideDown ? max - tmp : tmp + min;
} else {
uint div = range / span;
uint mod = range % span;
int tmp = pos * div + (2 * pos * mod + span) / (2 * span);
return upsideDown ? max - tmp : tmp + min;
}
// equiv. to min + (pos*range)/span + 0.5
// no overflow because of this implicit assumption:
// pos <= span < sqrt(INT_MAX+0.0625)+0.25 ~ sqrt(INT_MAX)
}
int QStyle::sliderPositionFromValue(int min, int max, int logicalValue, int span, bool upsideDown)
{
if (span <= 0 || logicalValue < min || max <= min)
return 0;
if (logicalValue > max)
return upsideDown ? span : min;
uint range = max - min;
uint p = upsideDown ? max - logicalValue : logicalValue - min;
if (range > (uint)INT_MAX/4096) {
double dpos = (double(p))/(double(range)/span);
return int(dpos);
} else if (range > (uint)span) {
return (2 * p * span + range) / (2*range);
} else {
uint div = span / range;
uint mod = span % range;
return p * div + (2 * p * mod + range) / (2 * range);
}
// equiv. to (p * span) / range + 0.5
// no overflow because of this implicit assumption:
// span <= 4096
}
然后是上方游标位置移动,部件布局如下设置left 或者right btn其位置是相对于frame而言, 又由于顶层窗口QWidget 采用布局 左右margin 为0 所以在顶层窗口QWidget 变化时, 相对于frame位置的偏移可不计, 所以move过程中产生的鼠标pos相对于最直接的QWidget 也是相对于frame最顶层的窗口,
这一段所讲只是由具体设计某个控件引发定义, 主要是考虑到子空间移动相关空间位置要明确其对照某个参考系而言, 或者将其转为对应参考系下的位置。
此外, 需要注意的是, 当双轴滑动条显示数据时候, 如果此控件能够要求窗口伸缩变化而变化,那么对于自定义的控件来说, 我们肯定要依据窗口变化而重新设计双轴滑动条的窗口, 一旦窗口长宽变化, 自然会引起原来的位置相对变化, 但由于其值是不变化的, 所以依据对应的计算值, 我们会发现在计算转换过程中如果其值是int 型 那么其误差就很大, 就会发现其移动一段距离还是原来int 值。所以在设计双轴滑动条时候, 能尽量保证显示的值在double并且精度越细越好, 这样误差才会更小。 因此保证精度细, 或者长宽不变是减小误差主要来源设计, 然后依据设计ui,合理设计相对位置坐标变化。
位置相关接口
void setGeometry(int x, int y, int w, int h)
void setGeometry(const QRect &)
此属性保存小部件相对于其父级的几何形状,不包括窗口框架
QPoint QMouseEvent::pos()
返回相对这个widget(重载了QMouseEvent的widget)的位置
QPoint QMouseEvent::globalPos()
窗口坐标,这个是返回鼠标的全局坐标
QPoint QCursor::pos() [static]
返回相对显示器的全局坐标
QPoint QWidget::mapToGlobal(const QPoint & pos) const
将窗口坐标转换成显示器坐标
QPoint QWidget::mapFromGlobal(const QPoint & pos) const
将显示器坐标转换成窗口坐标
QPoint QWidget::mapToParent(const QPoint & pos) const
将窗口坐标获得的pos转换成父类widget的坐标
QPoint QWidget::mapFromParent(const QPoint & pos) const
将父类窗口坐标转换成当前窗口坐标
QPoint QWidget::mapTo(const QWidget * parent, const QPoint & pos) const
将当前窗口坐标转换成指定parent坐标
QWidget::pos() : QPoint
返回相窗口的位置坐标
const QPointF & QMouseEvent::screenPos() const
以 QPointF 形式返回鼠标光标相对于接收事件的屏幕的位置。 和QPoint QMouseEvent::globalPos() 值相同,但是类型更高精度的QPointF
困惑一:QMouseEvent 中的 pos 是全局的吗 还是相对部件的
QMouseEvent 类包含描述鼠标事件的参数。 当在小部件内按下或释放鼠标按钮或移动鼠标光标时,会发生鼠标事件。
只有在按下鼠标按钮时才会发生鼠标移动事件,除非使用 QWidget::setMouseTracking() 启用鼠标跟踪。
当在小部件内按下鼠标按钮时,Qt 会自动抓取鼠标;小部件将继续接收鼠标事件,直到释放最后一个鼠标按钮。鼠标事件包含一个特殊的接受标志,指示接收者是否需要该事件。如果您的小部件未处理鼠标事件,您应该调用ignore()。鼠标事件沿父小部件链向上传播,直到小部件通过 accept() 接受它,或者事件过滤器使用它。
注意:如果鼠标事件被传播到一个已经设置了 Qt::WA_NoMousePropagation的小部件,该鼠标事件将不会在父小部件链上进一步传播。 可以通过调用从 QInputEvent 继承的 modifiers() 函数找到键盘修饰键的状态。函数 pos()、x() 和 y() 给出相对于接收鼠标事件的小部件的光标位置。如果由于鼠标事件而移动小部件,请使用 globalPos()返回的全局位置来避免晃动。
困惑二:QWidget 中的 pos 是全局的吗 还是相对部件的
此属性保存小部件在其父小部件中的位置
如果小部件是窗口,则位置是小部件在桌面上的位置,包括其框架。
更改位置时,小部件(如果可见)会立即接收移动事件 (moveEvent())。 如果小部件当前不可见,则保证在显示之前收到一个事件。
默认情况下,此属性包含引用原点的位置。
警告:在 moveEvent() 中调用 move() 或 setGeometry() 会导致无限递归。
Demo
如下顶层窗口top-level QWidget >> TestWidget 里面有个 MyTestButton;
顶层窗口TestWidget 重写了mouseMove事件。
void TestWidget::mouseMoveEvent(QMouseEvent *event)
{
ui->pushButton_2->move(event->pos());
qDebug() << __FUNCTION__ << event->pos() << event->globalPos();
}
截图
top-level QWidget 的 QMouseEvent globalPos() 返回的是全局的 基于屏幕的位置坐标。QMouseEvent pos返回的是相对接受控件TestWidget鼠标移动事件位置。
接下来,点击MyTestButton, 移动鼠标触发其对应的mouseMove事件, 发现输出QMouseEvent pos返回的是相对接受控件MyTestButton鼠标移动事件位置(相对于上一个之前他移动的位置为变化为35, 8)。 globalPos 仍然是 全局的 基于屏幕的位置坐标。
void MyTestButton::mouseMoveEvent(QMouseEvent *event)
{
qDebug() << __FUNCTION__ << event->pos() << event->globalPos();
}
返回的pos x = 35 = 960 -925; y = 8 = 582 -574; 所以我也不太建议在自身控件中mouseMoveEvent将使用this->move(event->pos); 因为这是不是很准确的移动所需要的位置。