gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
364 lines (322 loc) • 14 kB
JavaScript
/*
* DPI support for Windows 10
*
* Copyright 2016 Raising the Floor - US
*
* Licensed under the New BSD license. You may not use this file except in
* compliance with this License.
*
* You may obtain a copy of the License at
* https://github.com/GPII/universal/blob/master/LICENSE.txt
*/
"use strict";
/* There is no documented method to change the DPI in Windows 10. The Display Settings applet in Windows calls
* DisplayConfigSetDeviceInfo with an undocumented structure, which looks like the following:
*
* struct DISPLAYCONFIG_SET_DPI {
* DISPLAYCONFIG_DEVICE_INFO_HEADER header; // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553920.aspx
* INT32 dpiOffset; // see below
* };
*
* The 'type' member of the header, which is used to identify the type of packet, is (uint) -4.
*
* The procedure to getting the current DPI is similar. The "official" way of getting it is using
* GetDeviceCaps(LOGPIXELS[X/Y]), however that appears to only return the DPI that was set at the time of login.
* The Display Settings applet uses DisplayConfigGetDeviceInfo, and the packet looks something like:
*
* struct DISPLAYCONFIG_GET_DPI {
* DISPLAYCONFIG_DEVICE_INFO_HEADER header; // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553920.aspx
* INT32 minDpiOffset; // The minimum value of dpiOffset.
* INT32 dpiOffset; // see below
* INT32 maxDpiOffset; // The maximum value of dpiOffset, determined by the screen resolution.
* };
*
* The 'type' member of header is (uint) -3.
* maxDpiOffset is the maximum DPI of the current resolution, dpiOffset can be higher but not the actual DPI.
*
* The dpiOffset is the number of values away from the display's recommended value, and can be negative. The values
* (which are a percentage of 96 DPI), are:
* [ 100, 125, 150, 175, 200, 225, 250, 300, 350, 400, 450, 500 ]
* For example, if the recommended DPI is 175%, then a dpiOffset of 0 is 175, 2 is 225, and -1 is 150.
*/
var ref = require("ref");
var Struct = require("ref-struct");
var arrayType = require("ref-array");
var ffi = require("ffi-napi");
var fluid = require("gpii-universal");
var windows = fluid.registerNamespace("gpii.windows");
var t = windows.types;
var c = windows.API_constants;
// These API calls are for only Windows 10
var user32 = ffi.Library("user32", {
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff553903.aspx
"DisplayConfigGetDeviceInfo": [
// DISPLAYCONFIG_DEVICE_INFO_HEADER*
t.LONG, [ "pointer" ]
],
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff553909.aspx
"DisplayConfigSetDeviceInfo": [
// DISPLAYCONFIG_DEVICE_INFO_HEADER*
t.LONG, [ "pointer" ]
],
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff566772.aspx
"GetDisplayConfigBufferSizes": [
t.LONG, [ "uint32", "uint32*", "uint32*" ]
],
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff569215.aspx
"QueryDisplayConfig":[
t.LONG, [ "uint32", "uint32*", "pointer", "uint*", "pointer", "pointer" ]
]
});
// sizeof(DISPLAYCONFIG_MODE_INFO)
var DISPLAYCONFIG_MODE_INFO_SIZE = 64;
// THe types of packets send to DisplayConfigGetDeviceInfo/DisplayConfigSetDeviceInfo
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff553924.aspx
var DISPLAYCONFIG_DEVICE_INFO_TYPE = {
GET_SOURCE_NAME: 1,
GET_TARGET_NAME: 2,
GET_TARGET_PREFERRED_MODE: 3,
GET_ADAPTER_NAME: 4,
SET_TARGET_PERSISTENCE: 5,
GET_TARGET_BASE_TYPE: 6,
GET_SUPPORT_VIRTUAL_RESOLUTION: 7,
SET_SUPPORT_VIRTUAL_RESOLUTION: 8,
// undocumented:
GET_DPI: 0xfffffffd,
SET_DPI: 0xfffffffc
};
// https://msdn.microsoft.com/en-us/library/dd145066.aspx
windows.MONITORINFOEX = new Struct([
[t.DWORD, "cbSize"],
[arrayType(t.LONG, 4), "rcMonitor"],
[arrayType(t.LONG, 4), "rcWork"],
[t.DWORD, "dwFlags"],
[arrayType(t.TCHAR, c.CCHDEVICENAME), "szDevice"]
]);
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff553945.aspx
windows.DISPLAYCONFIG_PATH_INFO = new Struct([
[t.LUID, "source_adapterId"],
["uint32", "source_id"],
["uint32", "source_modeInfoIdx"],
["uint32", "source_statusFlags"],
[t.LUID, "target_adapterId"],
["uint32", "target_id"],
["uint32", "target_modeInfoIdx"],
["uint32", "target_outputTechnology"],
["uint32", "target_rotation"],
["uint32", "target_scaling"],
["uint32", "target_refreshRateNumerator"],
["uint32", "target_refreshRateDenominator"],
["uint32", "target_scanLineOrdering"],
[t.BOOL, "target_targetAvailable"],
["uint32", "target_statusFlags"],
["uint32", "flags"]
], { packed: true });
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff553933.aspx
// This structure serves no useful purpose (to GPII), so an anonymous blob of memory is used, instead of defining a
// handful of nested structures.
windows.DISPLAYCONFIG_MODE_INFO = new Struct([
[arrayType("char", DISPLAYCONFIG_MODE_INFO_SIZE), "reserved"]
]);
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff553920.aspx
windows.DISPLAYCONFIG_DEVICE_INFO_HEADER = new Struct([
// DISPLAYCONFIG_DEVICE_INFO_TYPE enum
["uint32", "type"],
["uint32", "size"],
[t.LUID, "adapterId"],
["uint32", "id"]
], { packed: true });
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff553983.aspx
windows.DISPLAYCONFIG_SOURCE_DEVICE_NAME = new Struct([
[windows.DISPLAYCONFIG_DEVICE_INFO_HEADER, "header"],
[arrayType(t.TCHAR, c.CCHDEVICENAME), "viewGdiDeviceName"]
]);
// This structure is not documented or defined in the SDK, but it's what control panel appears to use when querying the
// current DPI setting.
windows.DISPLAYCONFIG_GET_DPI = new Struct([
[windows.DISPLAYCONFIG_DEVICE_INFO_HEADER, "header"],
["int32", "minDpiOffset"],
["int32", "dpiOffset"],
["int32", "maxDpiOffset"]
], {packed: true});
// This structure is not documented or defined in the SDK, but it's what control panel appears to use when adjusting
// the DPI.
windows.DISPLAYCONFIG_SET_DPI = new Struct([
[windows.DISPLAYCONFIG_DEVICE_INFO_HEADER, "header"],
["int32", "dpiOffset"]
]);
/**
* Create and initialise a new new MONITORINFOEX structure
* @return {MONITORINFOEX} The structure.
*/
windows.display.createMonitorInfoStruct = function () {
var obj = new windows.MONITORINFOEX();
obj.ref().fill(0);
obj.cbSize = windows.MONITORINFOEX.size;
return obj;
};
/**
* Create and initialise a new DISPLAYCONFIG_SOURCE_DEVICE_NAME structure
* @return {DISPLAYCONFIG_SOURCE_DEVICE_NAME} The structure.
*/
windows.display.createSourceDeviceNameStruct = function () {
var obj = new windows.DISPLAYCONFIG_SOURCE_DEVICE_NAME();
obj.ref().fill(0);
obj.header.size = windows.DISPLAYCONFIG_SOURCE_DEVICE_NAME.size;
obj.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.GET_SOURCE_NAME;
return obj;
};
/**
* Create and initialise a new DISPLAYCONFIG_SET_DPI structure
* @return {DISPLAYCONFIG_SET_DPI} The structure.
*/
windows.display.createSetDpiStruct = function () {
var obj = new windows.DISPLAYCONFIG_SET_DPI();
obj.ref().fill(0);
obj.header.size = windows.DISPLAYCONFIG_SET_DPI.size;
obj.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.SET_DPI;
return obj;
};
/**
* Create and initialise a new DISPLAYCONFIG_GET_DPI structure
* @return {DISPLAYCONFIG_GET_DPI} The structure.
*/
windows.display.createGetDpiStruct = function () {
var obj = new windows.DISPLAYCONFIG_GET_DPI();
obj.ref().fill(0);
obj.header.size = windows.DISPLAYCONFIG_GET_DPI.size;
obj.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.GET_DPI;
return obj;
};
/**
* Gets the adapter and monitor (or socket?) id pair of a given device name, by iterating through all display
* configurations until there's a match.
* Inspired by https://msdn.microsoft.com/en-us/library/windows/hardware/dn690039.aspx
*
* @param {String} deviceName Device name of the display (eg, \\.\DISPLAY1).
* @return {Object} The adapterId and sourceId of the display.
*/
windows.display.getAdapter = function (deviceName) {
// Get the array sizes for the configuration
var pathCountPtr = ref.alloc("uint32");
var modeInfoCountPtr = ref.alloc("uint32");
var ret = user32.GetDisplayConfigBufferSizes(c.QDC_ONLY_ACTIVE_PATHS, pathCountPtr, modeInfoCountPtr);
windows.checkReturnCode(ret);
var pathCount = pathCountPtr.deref();
var modeInfoCount = modeInfoCountPtr.deref();
// Allocate the arrays
var PathInfoArray = arrayType(windows.DISPLAYCONFIG_PATH_INFO);
var ModeInfoArray = arrayType(windows.DISPLAYCONFIG_MODE_INFO);
var pathInfo = new PathInfoArray(pathCount);
var modeInfo = new ModeInfoArray(modeInfoCount);
// Get the display configurations
ret = user32.QueryDisplayConfig(c.QDC_ONLY_ACTIVE_PATHS, pathCountPtr, pathInfo.buffer, modeInfoCountPtr, modeInfo.buffer, ref.NULL);
windows.checkReturnCode(ret);
var adapterToGo = null;
// Find a matching display.
var sourceName = windows.display.createSourceDeviceNameStruct();
for (var n = 0; n < pathCount; n++) {
// Get the device name.
sourceName.header.adapterId = pathInfo[n].source_adapterId;
sourceName.header.id = pathInfo[n].source_id;
ret = user32.DisplayConfigGetDeviceInfo(sourceName.ref());
if (ret) {
// There was some problem with this display - ignore it and move on.
continue;
}
var name = windows.fromWideChar(sourceName.viewGdiDeviceName.buffer);
if (name === deviceName) {
// There could be more than one matching monitor (a clone). The first one is preferred, but if there's an
// internal/built-in display later, then let's use that one.
var isInternal =
(pathInfo[n].target_outputTechnology === c.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED) ||
(pathInfo[n].target_outputTechnology === c.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED) ||
(pathInfo[n].target_outputTechnology === c.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL);
if (adapterToGo === null || isInternal) {
adapterToGo = {
adapterId: sourceName.header.adapterId,
sourceId: sourceName.header.id
};
}
}
}
return adapterToGo;
};
/**
* Gets the device name of the primary monitor. (eg, \\.\DISPLAY1)
* @return {String} The device name of the primary monitor.
*/
windows.display.getPrimaryMonitorName = function () {
var monitor = windows.user32.MonitorFromWindow(ref.NULL_POINTER, c.MONITOR_DEFAULTTOPRIMARY);
var monitorInfo = windows.display.createMonitorInfoStruct();
windows.user32.GetMonitorInfoW(monitor, monitorInfo.ref());
return windows.fromWideChar(monitorInfo.szDevice.buffer);
};
/**
* Sets the DPI scale of the primary display, by specifying the number of values away from an from the recommended
* setting.
*
* @param {Number} offset The offset from the recommended setting.
* @return {DpiConfig} The newly configured, actual, and minimum/maximum DPI offsets (see getScreenDpi).
*/
windows.display.setScreenDpi = function (offset) {
// Only change the primary monitor, because that's what setScreenResolution does.
var monitorName = windows.display.getPrimaryMonitorName();
var adapter = windows.display.getAdapter(monitorName);
var setPacket = windows.display.createSetDpiStruct();
setPacket.header.adapterId = adapter.adapterId;
setPacket.header.id = adapter.sourceId;
if (offset < -3) {
// There isn't a lower limit, so add an artificial bottom cap. If the display supports lower settings then it
// would probably be unusable beyond this point.
offset = -3;
}
setPacket.dpiOffset = offset;
var ret = user32.DisplayConfigSetDeviceInfo(setPacket.ref());
windows.checkReturnCode(ret);
// Return the new configuration.
return windows.display.getScreenDpi(adapter);
};
/**
* Get the configured, maximum, and actual DPI values of the primary display.
*
* The value is the number of "notches" away from the recommended setting of the display.
*
* The configured scale is what DPI should be if the resolution is high enough, the maximum scale is the highest DPI
* scale that the current screen resolution supports. The actual scale (what the user is looking at) is the configured
* scale, capped at the maximum.
*
* @param {Object} adapter [optional] The adapter id pair (from getAdapter()).
* @return {DpiConfig} The newly configured, actual, and minimum/maximum DPI offsets
*/
windows.display.getScreenDpi = function (adapter) {
if (!adapter) {
var monitorName = windows.display.getPrimaryMonitorName();
adapter = windows.display.getAdapter(monitorName);
}
var getPacket = windows.display.createGetDpiStruct();
getPacket.header.adapterId = adapter.adapterId;
getPacket.header.id = adapter.sourceId;
var ret = user32.DisplayConfigGetDeviceInfo(getPacket.ref());
windows.checkReturnCode(ret);
var configured = getPacket.dpiOffset;
var maximum = getPacket.maxDpiOffset;
return {
configured: configured,
minimum: getPacket.minDpiOffset,
maximum: maximum,
actual: Math.min(configured, maximum)
};
};
/**
* DPI Configuration.
* @typedef {Object} DpiConfig
* @property {Number} configured The desired DPI setting.
* @property {Number} minimum The minimum available DPI setting.
* @property {Number} maximum The maximum available DPI setting.
* @property {Number} actual The actual DPI setting - the same as configured, but clamped to minimum and maximum.
*/
fluid.defaults("gpii.windows.display.getScreenDpi", {
gradeNames: "fluid.function",
argumentMap: {}
});