/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace AzQtComponents { namespace { std::string GetLastErrorString() { DWORD error = GetLastError(); if (!error) { return{}; } char* buffer = nullptr; DWORD length = FormatMessageA( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast(&buffer), 0, nullptr); if (!length) { return{}; } std::string result(buffer, length); LocalFree(buffer); return result; } using Hash = QHash; Hash* g_internalInstances = nullptr; // This is a POD type with size == sizeof(void*) QBasicMutex g_internalInstancesLock; } class ScreenGrabber::Internal { public: Internal(QSize size); virtual ~Internal(); QWidget* host() const { return m_host.data(); } HWND magnifier() const { return m_magnifier; } const QImage captured() const { return m_captured; } private: static BOOL WINAPI callback(HWND hwnd, void* srcdata, MAGIMAGEHEADER srcheader, void* destdata, MAGIMAGEHEADER destheader, RECT unclipped, RECT clipped, HRGN dirty); QScopedPointer m_host; HWND m_magnifier; QImage m_captured; }; ScreenGrabber::Internal::Internal(QSize size) { QMutexLocker locker(&g_internalInstancesLock); if (!g_internalInstances) { BOOL success = MagInitialize(); Q_UNUSED(success); AZ_Assert(success, "Failed to initialize Windows Magnification API: %s", GetLastErrorString().c_str()); } // Make a window to host the magnification "control" m_host.reset(new QWidget(nullptr, Qt::FramelessWindowHint)); m_host->setFixedSize(size / m_host->devicePixelRatioF()); m_host->show(); // move it to the top left m_host->move(0, 0); HWND hostHandle = reinterpret_cast(m_host->effectiveWinId()); // Add the WS_EX_LAYERED extended window style SetWindowLong(hostHandle, GWL_EXSTYLE, GetWindowLong(hostHandle, GWL_EXSTYLE) | WS_EX_LAYERED); // Set full opacity SetLayeredWindowAttributes(hostHandle, 0, 255, LWA_ALPHA); // Create the magnifier "control". It's really just a HWND that the API dumps pixels into. // We add 1 pixel to the size after observing that the callback function receives images of // the correct size, but with garbage data in the last scanline. We crop this in the callback. m_magnifier = CreateWindowW( WC_MAGNIFIERW, L"Open 3D Engine Color Picker Eyedropper Helper", WS_CHILD | WS_VISIBLE, 0, 0, size.width(), size.height() + 1, hostHandle, nullptr, nullptr, nullptr); // HACK; The magnifier control must be "visible" to function, which means it has to have some pixels on // screen. HOWEVER, we can set a clip region on the parent (layered) window which hides it entirely // and allows us to have it on screen without being visible. // We still receive all the pixels in the callback. RECT hostSize = {}; GetWindowRect(hostHandle, &hostSize); HRGN clip = CreateRectRgn(hostSize.right - hostSize.left, 0, hostSize.right - hostSize.left, hostSize.bottom - hostSize.top); SetWindowRgn(hostHandle, clip, true); if (!g_internalInstances) { g_internalInstances = new QHash(); } g_internalInstances->insert(m_magnifier, this); { // Although this mechanism is deprecated, we need to use it to get the pixel data because the GDI // functions that QScreen uses to read pixels don't work outside the visible desktop area. Ideally, // we'd be able to just read the pixels from the magnifier's HWND in the same way, but this just // results in white pixels due to the special way the magnification API writes the pixels to the // screen. BOOL success = MagSetImageScalingCallback(m_magnifier, &callback); Q_UNUSED(success) AZ_Assert(success, "Failed to initialize Windows Magnification imaging scaling callback: %s", GetLastErrorString().c_str()); } } ScreenGrabber::Internal::~Internal() { g_internalInstancesLock.lock(); g_internalInstances->remove(m_magnifier); if (g_internalInstances->isEmpty()) { delete g_internalInstances; g_internalInstances = nullptr; MagUninitialize(); } g_internalInstancesLock.unlock(); DestroyWindow(m_magnifier); } BOOL ScreenGrabber::Internal::callback(HWND hwnd, void* srcdata, MAGIMAGEHEADER srcheader, void* destdata, MAGIMAGEHEADER destheader, RECT unclipped, RECT clipped, HRGN dirty) { Q_UNUSED(srcheader); Q_UNUSED(destdata); Q_UNUSED(destheader); Q_UNUSED(unclipped); Q_UNUSED(clipped); Q_UNUSED(dirty); // We are actually given some Magnification API made child window of our "magnification control". HWND magnifier = GetAncestor(hwnd, GA_PARENT); QImage::Format format = QImage::Format_Invalid; if (srcheader.format == GUID_WICPixelFormat24bppRGB || srcheader.format == GUID_WICPixelFormat24bppBGR) { format = QImage::Format_RGB888; } else if (srcheader.format == GUID_WICPixelFormat32bppRGBA || srcheader.format == GUID_WICPixelFormat32bppBGRA) { format = QImage::Format_RGBA8888; } AZ_Assert(format != QImage::Format_Invalid, "Unable to convert pixel format"); // We subtract one from the height because the last scanline is always garbage auto result = QImage(((unsigned char*)srcdata) + srcheader.offset, srcheader.width, srcheader.height - 1, format); // With the above hack to make the magnifier window invisible, the R/B channels are inverted vs. // what the reported pixel format would suggest. If some of the pixels are visible, this doesn't // appear to be the case. if (srcheader.format != GUID_WICPixelFormat24bppBGR && srcheader.format != GUID_WICPixelFormat32bppBGRA) { result = result.rgbSwapped(); } // We hold the lock until the end of the function as we don't know what thread we're being // called back from and the UI thread may delete the instance QMutexLocker locker(&g_internalInstancesLock); if (!g_internalInstances) { AZ_Warning("ScreenGrabber", false, "Callback for unknown Magnification API control handle"); return false; } auto instance = g_internalInstances->find(magnifier); if (instance == g_internalInstances->end()) { AZ_Warning("ScreenGrabber", false, "Callback for unknown Magnification API control handle"); return false; } (*instance)->m_captured = result; return true; } ScreenGrabber::ScreenGrabber(const QSize size, Eyedropper* parent /* = nullptr */) : QObject(static_cast(parent)) , m_size(size) , m_owner(parent) { m_internal.reset(new Internal(size)); } ScreenGrabber::~ScreenGrabber() { m_internal.reset(); } QImage ScreenGrabber::grab(const QPoint& point) const { // We have to do this lazily, as we need the owning Eyedropper to have been shown // before we can get its HWND and exclude it. HWND hwnd = reinterpret_cast(m_owner->effectiveWinId()); BOOL success = MagSetWindowFilterList(m_internal->magnifier(), MW_FILTERMODE_EXCLUDE, 1, &hwnd); Q_UNUSED(success); AZ_Assert(success, "Couldn't add the grabber window to the magnification system's window filter list: %s", GetLastErrorString().c_str()); // The part of the screen we want to scale up QRect region({}, m_size); region.moveCenter(point); RECT area{ region.left(), region.top(), region.right(), region.bottom() }; success = MagSetWindowSource(m_internal->magnifier(), area); Q_UNUSED(success); AZ_Assert(success, "Couldn't update the part of the screen being magnified: %s", GetLastErrorString().c_str()); // Cause the magnification target to be redrawn InvalidateRect(m_internal->magnifier(), nullptr, true); return m_internal->captured(); } } // namespace AzQtComponents #include "Utilities/moc_ScreenGrabber.cpp"