#include #include #include #include #include #include #include #include #define NOMINMAX #include #include // This class implements the DirectManipulation windows api so that we can react to touch pad gestures properly. // See // - https://github.com/chromium/chromium/blob/main/content/browser/renderer_host/direct_manipulation_event_handler_win.cc // - https://github.com/desktop-app/lib_ui/blob/master/ui/platform/win/ui_windows_direct_manipulation.cpp class DirectManipulation : public QObject { public: enum class Type { ScrollStart, Scroll, ScrollStop, FlingStart, Fling, FlingStop, PinchStart, Pinch, PinchStop, }; struct Event { Type type{ Type::ScrollStart }; QPointF delta; }; public: DirectManipulation(QWidget* widget); ~DirectManipulation(); private: class Handler; class EventFilter; bool init(); void destroy(); void handlePointerHitTest(WPARAM wParam); bool eventFilter(QObject* watched, QEvent* event) override; void updateViewportSize(); void sendDirectManipulationEvent(const Event& event); QWidget* widget{ nullptr }; HWND hwnd{ nullptr }; Microsoft::WRL::ComPtr manager; Microsoft::WRL::ComPtr updateManager; Microsoft::WRL::ComPtr viewport; Microsoft::WRL::ComPtr handler; DWORD cookie{ 0 }; class QTimer* updateTimer{ nullptr }; }; class DirectManipulation::EventFilter : public QAbstractNativeEventFilter { public: EventFilter(DirectManipulation& parent) : parent(parent) { } bool nativeEventFilter(const QByteArray& eventType, void* message, qintptr*) override { MSG* msg = static_cast(message); if (msg) { if (msg->message == DM_POINTERHITTEST) { parent.handlePointerHitTest(msg->wParam); return true; } } return false; } DirectManipulation& parent; }; class DirectManipulation::Handler : public Microsoft::WRL::RuntimeClass< Microsoft::WRL::RuntimeClassFlags< Microsoft::WRL::RuntimeClassType::ClassicCom>, Microsoft::WRL::Implements< Microsoft::WRL::RuntimeClassFlags< Microsoft::WRL::RuntimeClassType::ClassicCom>, Microsoft::WRL::FtmBase, IDirectManipulationViewportEventHandler, IDirectManipulationInteractionEventHandler>> { public: void setParent(DirectManipulation* newParent) { parent = newParent; } void setViewportSize(QSize size) { width = size.width(); height = size.height(); } private: ~Handler() { } enum class State { None, Scroll, Fling, Pinch, }; void transitionToState(State newState) { if (state == newState) { return; } const State was = state; state = newState; switch (was) { case State::Scroll: { if (newState != State::Fling) { parent->sendDirectManipulationEvent({ Type::ScrollStop }); } } break; case State::Fling: { parent->sendDirectManipulationEvent({ Type::FlingStop }); } break; case State::Pinch: { parent->sendDirectManipulationEvent({ Type::PinchStop }); } break; default: break; } switch (newState) { case State::Scroll: { pendingScrollBegin = true; } break; case State::Fling: { parent->sendDirectManipulationEvent({ Type::FlingStart }); } break; case State::Pinch: { parent->sendDirectManipulationEvent({ Type::PinchStart }); } break; default: break; } } HRESULT STDMETHODCALLTYPE OnViewportStatusChanged(_In_ IDirectManipulationViewport* viewport, _In_ DIRECTMANIPULATION_STATUS current, _In_ DIRECTMANIPULATION_STATUS previous) override { if (current == previous) { return S_OK; } else if (current == DIRECTMANIPULATION_INERTIA) { if (previous != DIRECTMANIPULATION_RUNNING || state != State::Scroll) { return S_OK; } transitionToState(State::Fling); } if (current == DIRECTMANIPULATION_RUNNING) { if (previous == DIRECTMANIPULATION_INERTIA) { transitionToState(State::None); } } if (current != DIRECTMANIPULATION_READY) { return S_OK; } if (scale != 1.0f || xOffset != 0. || yOffset != 0.) { const HRESULT hr = viewport->ZoomToRect(0, 0, width, height, FALSE); if (!SUCCEEDED(hr)) { return hr; } } scale = 1.0f; xOffset = 0.0f; yOffset = 0.0f; transitionToState(State::None); return S_OK; } HRESULT STDMETHODCALLTYPE OnViewportUpdated(_In_ IDirectManipulationViewport* viewport) override { return S_OK; } HRESULT STDMETHODCALLTYPE OnContentUpdated(_In_ IDirectManipulationViewport* viewport, _In_ IDirectManipulationContent* content) override { float xform[6]; const HRESULT hr = content->GetContentTransform(xform, ARRAYSIZE(xform)); if (!SUCCEEDED(hr)) { return hr; } float newScale = xform[0]; float newXOffset = xform[4]; float newYOffset = xform[5]; if (newScale == 0.0f) { return hr; } if (qFuzzyCompare(newScale, scale) && newXOffset == xOffset && newYOffset == yOffset) { return hr; } if (qFuzzyCompare(newScale, 1.0f)) { if (state == State::None) { transitionToState(State::Scroll); } } else { transitionToState(State::Pinch); } const QPointF d = QPointF(newXOffset - xOffset, newYOffset - yOffset) / 2; if ((state == State::Scroll || state == State::Fling) && d.isNull()) { return S_OK; } if (state == State::Scroll) { if (pendingScrollBegin) { parent->sendDirectManipulationEvent({ Type::ScrollStart, d }); pendingScrollBegin = false; } else { parent->sendDirectManipulationEvent({ Type::Scroll, d }); } } else if (state == State::Fling) { parent->sendDirectManipulationEvent({ Type::Fling, d }); } else { qreal zoom = newScale / scale; zoom = zoom < 1 ? -(1 - zoom) : zoom - 1; parent->sendDirectManipulationEvent({ Type::Pinch, QPointF(0, zoom * 1000) }); } scale = newScale; xOffset = newXOffset; yOffset = newYOffset; return hr; } HRESULT STDMETHODCALLTYPE OnInteraction(_In_ IDirectManipulationViewport2* viewport, _In_ DIRECTMANIPULATION_INTERACTION_TYPE interaction) override { if (!parent) { return S_OK; } if (interaction == DIRECTMANIPULATION_INTERACTION_BEGIN) { parent->updateTimer->start(); } else if (interaction == DIRECTMANIPULATION_INTERACTION_END) { parent->updateTimer->stop(); } return S_OK; } DirectManipulation* parent{ nullptr }; State state{ State::None }; int width{ 0 }; int height{ 0 }; float scale{ 1.0f }; float xOffset{ 0.f }; float yOffset{ 0.f }; bool pendingScrollBegin{ false }; }; DirectManipulation::DirectManipulation(QWidget* widget) : QObject(widget) , widget(widget) , hwnd(reinterpret_cast(widget->window()->winId())) { if (init()) { widget->window()->installEventFilter(this); EventFilter* filter = new EventFilter(*this); QApplication::instance()->installNativeEventFilter(filter); updateTimer = new QTimer(this); updateTimer->setInterval(1000 / 120); updateTimer->setSingleShot(false); connect(updateTimer, &QTimer::timeout, this, [this] { if (updateManager) { updateManager->Update(nullptr); } }); } else { destroy(); } } DirectManipulation::~DirectManipulation() { destroy(); } bool DirectManipulation::init() { HRESULT hr = ::CoCreateInstance(CLSID_DirectManipulationManager, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&manager)); if (!manager) { return false; } hr = manager->GetUpdateManager(IID_PPV_ARGS(&updateManager)); if (!SUCCEEDED(hr) || !updateManager) { return false; } hr = manager->CreateViewport(nullptr, hwnd, IID_PPV_ARGS(&viewport)); if (!SUCCEEDED(hr) || !viewport) { return false; } const DIRECTMANIPULATION_CONFIGURATION configuration = DIRECTMANIPULATION_CONFIGURATION_INTERACTION | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA | DIRECTMANIPULATION_CONFIGURATION_SCALING; hr = viewport->ActivateConfiguration(configuration); if (!SUCCEEDED(hr)) { return false; } hr = viewport->SetViewportOptions(DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE); if (!SUCCEEDED(hr)) { return false; } handler = Microsoft::WRL::Make(); handler->setParent(this); hr = viewport->AddEventHandler(hwnd, handler.Get(), &cookie); if (!SUCCEEDED(hr)) { return false; } RECT rect = { 0, 0, 1000, 1000 }; hr = viewport->SetViewportRect(&rect); if (!SUCCEEDED(hr)) { return false; } updateViewportSize(); hr = manager->Activate(hwnd); if (!SUCCEEDED(hr)) { return false; } hr = viewport->Enable(); if (!SUCCEEDED(hr)) { return false; } hr = updateManager->Update(nullptr); if (!SUCCEEDED(hr)) { return false; } return true; } void DirectManipulation::destroy() { if (updateTimer) { updateTimer->stop(); } if (handler) { handler->setParent(nullptr); handler = nullptr; } if (viewport) { viewport->Stop(); if (cookie) { viewport->RemoveEventHandler(cookie); cookie = 0; } viewport->Abandon(); viewport = nullptr; } if (updateManager) { updateManager = nullptr; } if (manager) { manager->Deactivate(hwnd); manager = nullptr; } } void DirectManipulation::handlePointerHitTest(WPARAM wParam) { const UINT32 id = UINT32(GET_POINTERID_WPARAM(wParam)); POINTER_INPUT_TYPE type; if (::GetPointerType(id, &type) && type == PT_TOUCHPAD) { viewport->SetContact(id); } } bool DirectManipulation::eventFilter(QObject* watched, QEvent* event) { if (event->type() == QEvent::Resize) { updateViewportSize(); } return false; } void DirectManipulation::updateViewportSize() { const QRect r = QRect(QPoint(), widget->window()->size() * widget->devicePixelRatio()); handler->setViewportSize(r.size()); viewport->Stop(); const RECT rect = RECT{ r.left(), r.top(), r.left() + r.width(), r.top() + r.height() }; viewport->SetViewportRect(&rect); } void DirectManipulation::sendDirectManipulationEvent(const Event& event) { auto lookupModifiers = [] { const auto check = [](int key) { return (GetKeyState(key) & 0x8000) != 0; }; auto result = Qt::KeyboardModifiers(); if (check(VK_SHIFT)) { result |= Qt::ShiftModifier; } // NB AltGr key (i.e., VK_RMENU on some keyboard layout) is not handled. if (check(VK_RMENU) || check(VK_MENU)) { result |= Qt::AltModifier; } if (check(VK_CONTROL)) { result |= Qt::ControlModifier; } if (check(VK_LWIN) || check(VK_RWIN)) { result |= Qt::MetaModifier; } return result; }; auto send = [this, &event, lookupModifiers](Qt::ScrollPhase phase) { if (QWindow* windowHandle = widget->window()->windowHandle()) { POINT global = POINT(); ::GetCursorPos(&global); POINT local = global; ::ScreenToClient(hwnd, &local); const QPointF delta = event.delta * windowHandle->devicePixelRatio(); // This simulates scroll events with scroll phases like on macOS, which are handled by the graphics view. QWindowSystemInterface::handleWheelEvent( windowHandle, QPointF(local.x, local.y), QPointF(global.x, global.y), delta.toPoint(), delta.toPoint(), lookupModifiers() | (event.type == Type::Pinch ? Qt::ControlModifier : Qt::NoModifier), phase, Qt::MouseEventSynthesizedBySystem); } }; switch (event.type) { case Type::ScrollStart: send(Qt::ScrollBegin); break; case Type::Scroll: send(Qt::ScrollUpdate); break; case Type::FlingStart: case Type::Fling: send(Qt::ScrollMomentum); break; case Type::ScrollStop: send(Qt::ScrollEnd); break; case Type::FlingStop: send(Qt::ScrollEnd); break; default: send(Qt::NoScrollPhase); break; } } #include #include #include class GraphicsView : public QGraphicsView { public: using QGraphicsView::QGraphicsView; private: void wheelEvent(QWheelEvent* e) override { // Scroll phases are only available for mac trackpads and DirectManipulation on windows. const bool probablyTrackpad = e->phase() != Qt::ScrollPhase::NoScrollPhase; const bool shouldPan = !(e->modifiers() & Qt::CTRL) && probablyTrackpad; if (!shouldPan) { if (e->angleDelta().y()) { qreal zoom = e->angleDelta().y() * 0.002; if (zoom > 0) { scale(1 + zoom, 1 + zoom); } else if (zoom < 0) { scale(1.0 / (1 + std::fabs(zoom)), 1.0 / (1 + std::fabs(zoom))); } } } else { QPointF delta = e->pixelDelta(); if (delta.isNull()) { delta = e->angleDelta(); } translate(delta.x(), delta.y()); } } }; int main(int argc, char** argv) { QApplication app(argc, argv); QGraphicsScene scene; scene.setSceneRect(-400, -400, 800, 880); scene.addEllipse(QRect(-50, -50, 100, 100)); scene.addEllipse(QRect(-75, -75, 150, 150)); scene.addEllipse(QRect(-100, -100, 200, 200)); scene.addEllipse(QRect(-150, -150, 300, 300)); GraphicsView view(&scene); view.resize(300, 300); view.centerOn(0, 0); view.setTransformationAnchor(QGraphicsView::NoAnchor); new DirectManipulation(&view); view.show(); return app.exec(); }