我们公司的软件采用WPF开发,最近需要迁移到国产操作系统上运行,通过方案对比,最后采用wine直接进行适配。
在我们的软件中有一个功能,就是获取当前屏幕的模式,并可以设置屏幕的模式
在适配后发现linux中运行时功能部分有效,部分无效,具体如下:
- 获取当前屏幕数量:成功
- 获取屏幕模式:失败
- 设置屏幕模式:失败
一、当前WPF中相应功能的代码
1. 获取屏幕数量(wine适配有效)
/// <summary>
/// 获取是否多屏
/// </summary>
/// <returns></returns>
public static bool GetIsMultiScreen()
{
var status = GetDisplayConfigBufferSizes(
QueryDisplayFlags.DatabaseCurrent,
out var numPathArrayElements,
out _);
return numPathArrayElements > 1;
}
[DllImport("User32.dll")]
public static extern StatusCode GetDisplayConfigBufferSizes(
QueryDisplayFlags flags,
out int numPathArrayElements,
out int numModeInfoArrayElements);
[Flags]
public enum QueryDisplayFlags : uint
{
Zero = 0x0,
AllPaths = 0x00000001,
OnlyActivePaths = 0x00000002,
DatabaseCurrent = 0x00000004
}
2. 获取屏幕模式(wine适配失败)
/// <summary>
/// 获取是否是扩展模式
/// </summary>
/// <returns></returns>
public static bool GetIsExtentModel()
{
var id = GetCurrentMode();
return id == DisplayConfigTopologyId.Extend;
}
private static DisplayConfigTopologyId GetCurrentMode(
QueryDisplayFlags pathType = QueryDisplayFlags.DatabaseCurrent)
{
DisplayConfigTopologyId topologyId = DisplayConfigTopologyId.Zero;
int numPathArrayElements;
int numModeInfoArrayElements;
var status = GetDisplayConfigBufferSizes(
pathType,
out numPathArrayElements,
out numModeInfoArrayElements);
if (status != StatusCode.Success)
{
//POSSIBLY HANDLE SOME OF THE CASES.
var reason = string.Format("GetDisplayConfigBufferSizesFailed() failed. Status: {0}", status);
throw new Exception(reason);
}
var pathInfoArray = new DisplayConfigPathInfo[numPathArrayElements];
var modeInfoArray = new DisplayConfigModeInfo[numModeInfoArrayElements];
var queryDisplayStatus = pathType == QueryDisplayFlags.DatabaseCurrent
? QueryDisplayConfig(
pathType,
ref numPathArrayElements, pathInfoArray,
ref numModeInfoArrayElements, modeInfoArray, out topologyId)
: QueryDisplayConfig(
pathType,
ref numPathArrayElements, pathInfoArray,
ref numModeInfoArrayElements, modeInfoArray);
if (queryDisplayStatus != StatusCode.Success)
{
// POSSIBLY HANDLE SOME OF THE CASES.
var reason = string.Format("QueryDisplayConfig() failed. Status: {0}", queryDisplayStatus);
throw new Exception(reason);
}
return topologyId;
}
[DllImport("User32.dll")]
private static extern StatusCode QueryDisplayConfig(
QueryDisplayFlags flags,
ref int numPathArrayElements,
[Out] DisplayConfigPathInfo[] pathInfoArray,
ref int modeInfoArrayElements,
[Out] DisplayConfigModeInfo[] modeInfoArray,
IntPtr topologyId = default
);
[Flags]
private enum DisplayConfigTopologyId : uint
{
Zero = 0x0,
Internal = 0x00000001,
Clone = 0x00000002,
Extend = 0x00000004,
External = 0x00000008,
}
public enum StatusCode : uint
{
Success = 0,
InvalidParameter = 87,
NotSupported = 50,
AccessDenied = 5,
GenFailure = 31,
BadConfiguration = 1610,
InSufficientBuffer = 122,
}
[StructLayout(LayoutKind.Sequential)]
private struct DisplayConfigPathInfo
{
public DisplayConfigPathSourceInfo sourceInfo;
public DisplayConfigPathTargetInfo targetInfo;
public DisplayConfigFlags flags;
}
[StructLayout(LayoutKind.Sequential)]
private struct DisplayConfigPathSourceInfo
{
public LUID adapterId;
public uint id;
public uint modeInfoIdx;
public DisplayConfigSourceStatus statusFlags;
}
3. 设置屏幕模式(wine适配失败)
/// <summary>
/// 设置显示器复制模式
/// </summary>
public static void SetDisplayCloneModel()
{
SetDisplayModel(DisplayConfigTopologyId.Clone);
}
/// <summary>
/// 设置显示器扩展模式
/// </summary>
public static void SetDisplayExtentModel()
{
SetDisplayModel(DisplayConfigTopologyId.Extend);
}
/// <summary>
/// 设置显示模式
/// </summary>
/// <param name="id"></param>
private static void SetDisplayModel(DisplayConfigTopologyId id)
{
//最终决定使用老代码 bb 2021-03-15 12:00:46
SetDisplayModelOld(id);
//调用新的操作设置显示模式 bb 2021-03-04 10:10:09
//SetDisplayModelNew(id);
}
/// <summary>
/// 设置显示器模式
/// </summary>
/// <param name="id"></param>
private static void SetDisplayModelOld(DisplayConfigTopologyId id)
{
SetDisplayConfig(0, IntPtr.Zero, 0, IntPtr.Zero, SDC_APPLY | (uint)id);
}
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern long SetDisplayConfig(uint numPathArrayElements, IntPtr pathArray,uint numModeArrayElements,IntPtr modeArray, uint flags);
private const uint SDC_APPLY = 0x00000080;
二、问题排查和分析
1. 获取屏幕数量
没有问题,不需要排查
2. 获取屏幕模式
发现在调用GetIsExtentModel方法时,返回永远是false(复制模式),通过跟踪定位,发现问题是出在QueryDisplayConfig这个方法上,在调用这个方法返回状态位success,但是topologyId参数一直返回DisplayConfigTopologyId.Internal,查看win的源码如下:
wine github地址
仔细分析了下源代码,发现代码只是做了参数判断,实际上没有真正执行获取屏幕模式的代码:
结论:采用QueryDisplayConfig方法获取屏幕模式在wine中没有适配,暂不能使用,需要考虑替代方案
3. 设置屏幕模式
设置屏幕模式采用的SetDisplayConfig方法,通过监控发现,方法返回成功,但是设置没有生效,通过wine源码查看发现,源码只是对方法进行了打桩,并没有真实适配:
结论:设置屏幕模式的代码在wine中没有适配,暂不能使用,需要考虑替代方案
二、解决方案
1. 获取屏幕数量
没有问题,不需要解决
2. 获取屏幕模式
通过查找,发现获取屏幕模式还可以通过变相的解决方案:
采用GetSystemMetrics获取主屏和扩展屏的坐标,判断是否是复制模式:
/// <summary>
/// 获取是否是扩展模式
/// </summary>
/// <returns></returns>
private bool GetIsExtentModel()
{
int cxVirtual =GetSystemMetrics(SM_CXVIRTUALSCREEN);
int cyVirtual = GetSystemMetrics(SM_CYVIRTUALSCREEN);
int cxScreen = GetSystemMetrics(SM_CXSCREEN);
int cyScreen = GetSystemMetrics(SM_CYSCREEN);
// 如果虚拟屏幕的尺寸等于单个屏幕的尺寸,则可能是复制模式
if (cxVirtual == cxScreen && cyVirtual == cyScreen)
{
// 可能是复制模式
return true;
}
// 如果虚拟屏幕的尺寸大于单个屏幕的尺寸,则可能是扩展模式
else
{
// 可能是扩展模式
return false;
}
}
[DllImport("user32.dll")]
public static extern int GetSystemMetrics(int nIndex);
private const int SM_CYSCREEN = 1;
private const int SM_CXSCREEN = 0;
private const int SM_CYVIRTUALSCREEN = 79;
private const int SM_CXVIRTUALSCREEN = 78;
3. 设置屏幕模式
网上有说采用ChangeDisplaySettingsEx来间接设置屏幕模式的办法,但是我没有找到一个有效的方案,我这里指定另一种解决方案:
采用linux原生命令来修改屏幕模式。
使用Xrandr命令来设置复制模式和扩展模式
//设置扩展模式命令
xrandr --output HDMI-1 --auto --right-of LVDS-1
//设置复制模式命令
xrandr --output VGA-1 --same-as HDMI-1
如果你需要在C++程序中调用 xrandr 命令,可以使用 std::system 函数:
#include <cstdlib>
void setCloneMode()
{
//设置扩展模式命令
system("xrandr --output HDMI-1 --auto --right-of LVDS-1");
//设置复制模式命令
system("xrandr --output VGA-1 --same-as HDMI-1");
}
xrandr详细说明如下:xrandr 命令,Linux xrandr 命令详解
注:虽然xrandr能够完成模式切换,但是xrandr切换为扩展模式时需要手动指定2个屏幕的左右方向,许多场景是不满足的 。