You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
259 lines
9.4 KiB
C++
259 lines
9.4 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#include <AzCore/PlatformIncl.h>
|
|
#include <Magnification.h>
|
|
|
|
#include <QApplication>
|
|
#include <QScreen>
|
|
#include <QWindow>
|
|
#include <QtWinExtras/QtWin>
|
|
#include <QGlobalStatic>
|
|
#include <QHash>
|
|
#include <QMutex>
|
|
#include <QtMath>
|
|
|
|
#include <string>
|
|
#include <AzCore/Debug/Trace.h>
|
|
|
|
#include <AzQtComponents/Components/Widgets/Eyedropper.h>
|
|
#include <AzQtComponents/Utilities/ScreenGrabber.h>
|
|
|
|
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<LPSTR>(&buffer),
|
|
0,
|
|
nullptr);
|
|
if (!length)
|
|
{
|
|
return{};
|
|
}
|
|
|
|
std::string result(buffer, length);
|
|
LocalFree(buffer);
|
|
return result;
|
|
}
|
|
|
|
using Hash = QHash<HWND, ScreenGrabber::Internal*>;
|
|
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<QWidget> 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<HWND>(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<HWND, ScreenGrabber::Internal*>();
|
|
}
|
|
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(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<HWND>(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"
|