Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >浅谈window桌面GUI技术及图像渲染性能测试实践

浅谈window桌面GUI技术及图像渲染性能测试实践

作者头像
高楼Zee
发布于 2019-07-17 09:23:31
发布于 2019-07-17 09:23:31
4.1K00
代码可运行
举报
文章被收录于专栏:7DGroup7DGroup
运行总次数:0
代码可运行

Windows的图形界面架构

Windows Vista之后,desktop composition的部分就由Desktop Window Manager完成了(当然是启用Aero的情况下,Windows 8起DWM是必须开启的)

如上图,应用程序画完了界面,告诉DWM把它放到桌面上去

DWM本身是基于Direct3D的,D3D下面是WDDM驱动

至于应用程序,绝大多数win桌面应用都是基于GDI的,很老的图形库 (从某个版本起GDI也是跑在D3D之上了,于是显卡厂家就不用写GDI驱动了),D3D(比如基于WPF的应用,今天主要介绍的应用),OpenGL(现在的Windows的图形架构是以DirectX为主,OpenGL支持需要OpenGL installable client driver)

window图像渲染的基本流程

从程序中提交一个Draw,数据需要经过:

App->DX runtime->User mode driver->dxgkrnl->Kernel mode driver->GPU

在到达GPU之前,全都是在CPU上执行的,所以从程序本身是无法获取渲染结果

到这里就为我们做window桌面程序图像渲染性能测试带来两个问题:

  • 怎么检查图像渲染的质量?
  • 怎么获取图像渲染的响应时间?

由于需要桌面UI自动化测试的技术,所以下面我们介绍window桌面程序UI自动化测试技术

window桌面程序UI自动化测试技术

Win32程序

使用 Win32 API 来创建的程序成为Win32程序。 提供 Win32 API的dll被加载到应用程序的进程中,应用程序通过这些API来创建线程、窗口和控件。 Win32程序中,所有窗口和控件都是一个窗口类的实例,都拥有一个窗口句柄,窗口对象属于内核对象,由Windows子系统来维护。

Windows子系统为标准控件定义了窗口类,并使用GDI来绘制这些标准控件。 Win32程序采用消息循环机制:

WPF程序

WPF的控件不再是通过Win32 API来创建窗口,使用Win32 API并不能查找和操作WPF控件 WPF所有控件和动画都是使用DirectX 绘制 WPF控件不直接支持MSAA,而是通过 UIA 用桥转换技术来支持MSAA WPF用AutomationPeer类支持自动化,每一种控件都有对应的 AutomationPeer类。AutomationPeer不直接暴露给测试客户端,而是通过UIA来使用。UIA向应用程序窗口发送WM_GetObject消息,获得由AutomationPeer实现的UIA Server端Provider。AutomationPeer由控件创建(OnCreateAutomationPeer)

UIAutomation

UIAutomation是微软从Windows Vista开始推出的一套全新UI自动化测试技术, 简称UIA。

UIA定义了全新的、针对UI自动化的接口和模式。测试程序可以通过这些接口来查找和操作控件。 遍历和条件化查询:TreeWalker/FindAll UI元素属性的UIA Property, 包括Name、 ID、Type、ClassName、Location、 Visibility等等。 UIA Pattern(控件的行为模式), 比如Select、Expand、Resize、 Check、Value等等。

UIA的两种实现方法: Server-Side Provider:

由被测程序实现UIA定义的接口,返回给测试程序。WPF程序通过这种方式来支持UIA。

Client-Side Provider:

测试程序没有实现UIA定义的接口。由UIA Runtime或测试程序自己来实现。比如Win32和WinForm程序,UIA Runtime通过MSAA来实现UIA定义的接口。UIA定义了全新的、针对UI自动化的接口和模式。测试程序可以通过这些接口来查找和操作控件。 遍历和条件化查询:TreeWalker/FindAll UI元素属性的UIA Property, 包括Name、 ID、Type、ClassName、Location、 Visibility等等。 UIA Pattern(控件的行为模式), 比如Select、Expand、Resize、 Check、Value等等。

UIA驱动计算器示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
using System.Windows.Automation;

PropertyCondition conditionName = new PropertyCondition(AutomationElement.NameProperty, "计算器");
AutomationElement calcWindow =  AutomationElement.RootElement.FindFirst(TreeScope.Children, conditionName);

//Button 1
PropertyCondition conditionBtn1 = new PropertyCondition(AutomationElement.AutomationIdProperty, "131");
AutomationElement button1 = calcWindow.FindFirst(TreeScope.Descendants, conditionBtn1);

//点击Button1
InvokePattern invokePatternBtn1 = (InvokePattern)button1.GetCurrentPattern(InvokePattern.Pattern);
invokePatternBtn1.Invoke();

桌面程序图像渲染性能测试实践

因为我们的性能测试是基于部分UI自动化测试技术落地的,在此介绍一下我们的UI自动化测试解决方案

测试解决方案应至少包括5个项目,其中前两个是和其他测试解决方案共享的。5个项目均为类库,不能直接执行。

  • AI.Robot为UI驱动框架。
  • AI.Utilities 项目里是一些辅助类,如数据库读写、图片对比等(性能测试需用到)。
  • AI.App.UIObjects项目里放置UI对象。把UI对象集中放置到此项目中是为了减少界面更改带来的维护工作量。
  • AI.App.BusinessLogic项目里放置可重复用到的界面元素操作的集合,通常是为了完成一项特定的业务的步骤的集合。
  • AI.App.TestCases里放置测试用例。并按照MSTest单元测试框架组织测试类和测试方法。包含测试类和测试方法的.net类库称为测试程序集。

今天讨论的桌面程序图像渲染性能测试主要应用于以下两种应用:

  • 普通PC桌面WPF应用(分辨率<2K)
  • 大屏幕可视化WPF应用(分辨率>8K)

普通PC桌面WPF应用

首先,回到之前的两个问题:

  • 怎么检查图像渲染的质量?
  • 怎么获取图像渲染的响应时间?

首先将正常渲染完的控件输出成图片

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 将控件uiElement输出到图片aa.bmp
uiElement.CaptureBitmap(@"D:\aa.bmp");

使用测试工具驱动启动被测应用并开始计时,在渲染过程中快速截图,实时比较两幅图片是否完全相等,如果相等并结束计时并写入响应时间。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 比较两幅图片是否完全相同(所有像素点都相同)
bool isEqual = ImageHelper.IsEqual(@"D:\image1.bmp", @"D:\image2.bmp");

判断两幅图是否完全相同

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/// <summary>
/// 判断两幅图是否完全相同
/// </summary>
/// <param name="imageFile1">待比较的第一幅图</param>
/// <param name="imageFile2">待比较的第二幅图</param>
/// <returns>如果两幅图完全相等,则返回true,否则返回false</returns>
public static bool IsEqual(string imageFile1, string imageFile2)
{
   float similarity = Compare(imageFile1, imageFile2);
   return similarity == 1;
 }

影响图片输出的因素:

  • 显卡,不同显卡输出文字和渐变色的时候有细微的差别,所以不同机器上显示的控件和输出的图片通常不完全相同,特别是当控件上有文字的时候。
  • DPI设置,将机器的DPI设置为120%时,100x100大小的控件将显示为120x120像素
  • 当在远程桌面上运行测试时,远程连接的选项“字体平滑”会影响控件显示和输出的图片

大屏幕可视化WPF应用

由于大屏幕的分辨率8K起步,也就不适应上面的截图判断方法了,为什么呢? 我们简单来计算8K图片的大小吧

分辨率:7680×4320=33177600像素≈95MB

我们常见显示器用256种状态标识屏幕上某种颜色的灰度,而屏幕采用三基色红绿蓝(RGB),不压缩的情况下一个像素需要占用24bit(3字节),这个就是常说的24位真彩色。

近100MB的图片实时截图并进行判断,本身两个动作就会对机器的计算资源消耗巨大,会严重影响性能测试准确性。

这里我们折中使用实时判断标志位RGB像素点的方法来判断图片渲染的结果

首先,我们会使用取色器采样几个最后图像渲染完成的坐标像素点RGB值 原理其实很简单,只需要两步

  • 鼠标移动的时候获取鼠标光标的位置
  • 鼠标单击获取当前鼠标光标的位置的RGB颜色值到粘贴板

涉及HookManager技术

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
namespace GetColor
{
    public partial class Form1 : Form
    {
        //显示设备上下文环境的句柄
        private IntPtr hdc = IntPtr.Zero;
        private int maxY, maxX;

        public Form1()
        {
            InitializeComponent();

            this.BackColor = Color.Brown;
            this.TransparencyKey = Color.Brown;

            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
            this.WindowState = FormWindowState.Maximized;
            this.TopMost = true;

            this.Cursor = Cursors.Cross;
            HookManager.MouseMove += OnMouseMove;
            this.KeyPress += OnKeyPress;
            this.MouseClick += OnMouseDown;

            Size screenSize = Screen.PrimaryScreen.Bounds.Size;
            this.maxX = screenSize.Width - this.label1.Width;
            this.maxY = screenSize.Height - this.label1.Height;

            this.SetLabel(Control.MousePosition.X, Control.MousePosition.Y);
        }

        //鼠标移动,实时获取鼠标位置
        private void OnMouseMove(object sender, MouseEventArgs e)
        {
             this.SetLabel(Control.MousePosition.X, Control.MousePosition.Y);

        }

        private void SetLabel(int x, int y)
        {
            if(this.hdc == IntPtr.Zero)
            {
                this.hdc = GetDC(IntPtr.Zero);
                this.label1.Location = new Point(Math.Min(this.maxX, x + 10), Math.Min(this.maxY, y + 10));
                int color = GetPixel(this.hdc, x, y);
                this.label1.Text = string.Format("X={0},Y={1}\r\nColor:#{2}\r\n{3}", x, y, Convert.ToString(color, 16).PadLeft(6, '0'), color);
                this.Update();
                ReleaseDC(IntPtr.Zero, this.hdc);
                DeleteDC(this.hdc);

                this.hdc = IntPtr.Zero;
            }

        }

        private void OnKeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == (char)Keys.Escape)
            {
                UnHook();
                Application.Exit();
            } 
        }

        private void OnMouseDown(object sender, MouseEventArgs e)
        {
            //检索一指定窗口的客户区域或整个屏幕的显示设备上下文环境的句柄
            this.hdc = GetDC(IntPtr.Zero);
            //指定坐标点的像素的RGB颜色值。
            int color = GetPixel(this.hdc, e.X, e.Y);
            //鼠标单击拷贝值
            if (e.Button == MouseButtons.Left)
            {
                Clipboard.SetText(string.Format("<Point x=\"{0}\" y=\"{1}\" color=\"{2}\"/>", e.X, e.Y, color));
            }
            ReleaseDC(IntPtr.Zero, this.hdc);
            DeleteDC(this.hdc);

            UnHook();
            Application.Exit();
        }

        private void UnHook()
        {
            HookManager.MouseMove -= OnMouseMove;
        }

        [DllImport("user32")]
        public static extern IntPtr GetDC(IntPtr hwnd);

        [DllImport("gdi32")]
        public static extern int GetPixel(IntPtr hdc, int x, int y);

        [DllImport("user32.dll")]
        public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDc);

        [DllImport("gdi32.dll")]
        public static extern IntPtr DeleteDC(IntPtr hDc);
    }
}

小程序截图:

把图像渲染结果采样点填入测试工具的XML配置文件后,我们使用测试工具启动程序开始计时并实判断采样标志位像素点的RGB值,如果全部通过结束计时并写入渲染响应时间

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void ValidateStage(Screen screen)
        {            
            bool allReady = false;
            foreach (var stage in screen.ValidationStages)
            {
                stage.IsReady = false;
            }

            DateTime now = DateTime.Now;
            while (!allReady && (DateTime.Now - now).TotalSeconds < this.Config.Timeout)
            {
                allReady = true;
                foreach (var stage in screen.ValidationStages)
                {
                    if(!stage.IsReady)
                    {
                        stage.IsReady = this.ValidatePointsOneTime(stage.ValidatePoints);

                        if (stage.IsReady)
                        {
                            TimeSpan cost = DateTime.Now - now;
                            this.logger.LogInfo(string.Format("{0}耗时{1}", stage.Name, cost.TotalSeconds));
                            this.logData += string.Format(",{0}", cost.TotalSeconds);
                        }
                        else
                        {
                            allReady = false;
                        }
                    }                    
                }

                Thread.Sleep(this.Config.TryInterval);
            }

            foreach (var stage in screen.ValidationStages)
            {
                if(!stage.IsReady)
                {
                    foreach (ValidatePoint point in stage.ValidatePoints)
                    {
                        int color = Root.GetPointColor(point.X, point.Y);
                        this.logger.LogInfo(string.Format("点({0}, {1})的颜色为{2}", point.X, point.Y, color));
                    }

                    this.logger.LogInfo(string.Format("{0}秒内{1}未绘制完", this.Config.Timeout.ToString(), stage.Name));
                    this.logData += string.Format(",>{0}", this.Config.Timeout);
                }
            }
        }

        public void ValidateStage(Screen screen, ValidationStage stage)
        {
            DateTime now = DateTime.Now;

            bool screenReady = this.ValidatePoints(stage.ValidatePoints);
            if (screenReady)
            {
                TimeSpan cost = DateTime.Now - now;
                this.logger.LogInfo(string.Format("{0}耗时{1}", stage.Name, cost.TotalSeconds));
                this.logData += string.Format(",{0}", cost.TotalSeconds);
            }
            else
            {
                this.logger.LogInfo(string.Format("{0}秒内{1}未绘制完", this.Config.Timeout.ToString(), stage.Name));
                this.logData += string.Format(",>{0}", this.Config.Timeout);
            }
        }

        public bool ValidatePointsOneTime(List<ValidatePoint> validationPoints)
        {
            foreach (ValidatePoint point in validationPoints)
            {
                int color = Root.GetPointColor(point.X, point.Y);
                if (!Common.IsSimilarColor(color, point.Color))
                {
                    return false;
                }
            }

            return true;
        }

        public bool ValidatePoints(List<ValidatePoint> validationPoints)
        {
            DateTime endTime = DateTime.Now + TimeSpan.FromSeconds(this.Config.Timeout);
            bool finished = false;
            while (DateTime.Now < endTime)
            {
                bool allPointReady = true;
                foreach (ValidatePoint point in validationPoints)
                {
                    int color = Root.GetPointColor(point.X, point.Y);
                    //this.logger.LogInfo(color.ToString() + "   " + point.Color.ToString());
                    if (!Common.IsSimilarColor(color, point.Color))
                    {
                        allPointReady = false;
                        break;
                    }
                }

                if (allPointReady)
                {
                    finished = true;
                    break;
                }
                else
                {
                    System.Threading.Thread.Sleep(this.Config.TryInterval);
                }
            }

            if(!finished)
            {
                foreach (ValidatePoint point in validationPoints)
                {
                    int color = Root.GetPointColor(point.X, point.Y);
                    this.logger.LogInfo(string.Format("点({0}, {1})的颜色为{2}", point.X, point.Y, color));
                }                
            }

            return finished;
        }

实际效果:

参考资料:

Windows Display Driver Model (WDDM) Architecture (Windows Drivers)

The Desktop Window Manager (Windows)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-08-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 7DGroup 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
UI自动化 --- 微软UI Automation
无论是接口自动化测试,还是UI自动化测试,目的就是为了提高产品的稳定性,保证用户体验。
Niuery Diary
2023/10/22
1.5K0
UI自动化 --- 微软UI Automation
WPF 使用 Silk.NET 进行 DirectX 渲染入门
本文告诉大家如何使用 dotnet 基金会新开源的 Silk.NET 库调用 DirectX 进行渲染的方法。此库是对 DirectX 的底层基础封装,用上了 dotnet 和 C# 的各个新特性,相对来说基础性能较好,也许后续可以考虑作为 SharpDx 的代替
林德熙
2021/12/27
3.2K0
WPF 使用 Silk.NET 进行 DirectX 渲染入门
WPF 渲染原理
在 WPF 最主要的就是渲染,因为 WPF 是一个界面框架。想用一篇博客就能告诉大家完整的 WPF 渲染原理是不可能的。本文告诉大家 WPF 从开发者告诉如何画图像到在屏幕显示的过程。本文是从一个很高的地方来看渲染的过程,在本文之后会添加很多博客来告诉大家渲染的细节。
林德熙
2018/09/19
3K0
WPF 渲染原理
扫描仪对接(C#)
源代码地址:http://www.codeproject.com/Articles/171666/Twain-for-WPF-Applications-Look-Ma-No-Handles
码客说
2022/09/19
4.9K0
扫描仪对接(C#)
dotnet 读 WPF 源代码笔记 启动欢迎界面 SplashScreen 的原理
本文是我在读 WPF 源代码做的笔记。在 WPF 中的启动界面,为了能让 WPF 的启动界面显示足够快,需要在应用的 WPF 主机还没有启动完成之前就显示出启动图,此时的启动图需要自己解析图片同时也需要自己创建显示窗口
林德熙
2020/12/22
1.2K0
WPF 源代码 从零开始写一个 UI 框架
需要知道 WPF 是一个 UI 框架,作为一个 UI 框架,最主要的就是交互。也就是 UI 框架需要有渲染显示和处理用户输入的功能。 如果直接告诉大家 WPF 里面有哪些类,估计没有几位小伙伴会听下去,要么就是讲的类太简单,看过去我也就知道了,要么就是这个类可能我一直都不会用到他,即使可能会用到也早就忘了。 本文不会直接告诉大家 WPF 的源代码是如何写的,而是从零开始一起来写一个 UI 框架,在写的过程就会了解到为什么 WPF 可以这样写,为什么需要这样写,和 WPF 这样写的好处。 本文适合 WPF 的开发者同样也适合其他语言希望自己写一个 UI 框架的小伙伴。
林德熙
2019/03/13
3.9K2
WPF 源代码 从零开始写一个 UI 框架
WPF 模拟 WPFMediaKit 的 D3D 配置用来测试4k性能
本文告诉大家我在测试 WPFMediaKit 的 D3D 配置性能影响在 4k 分辨率设备下采用高清摄像头的性能
林德熙
2021/12/23
1.1K0
WPF 使用GDI+提取图片主色调并生成Mica材质特效背景
TwilightLemon/MicaImageTest: WPF 使用GDI+提取图片主色调并生成Mica材质特效背景
郑子铭
2025/07/02
420
WPF 使用GDI+提取图片主色调并生成Mica材质特效背景
WPF写的取色器
昨天有个小伙子,在找取色器工具。我说,这个应该开发起来很简单,于是,摸了大约半个钟的鱼,开发了一个。现在我把源码和操作案例发出来,供有需要的大佬们玩。(功能过于单一和简单,但是能用)
Wesky
2024/08/13
1500
WPF写的取色器
(五十一)c#Winform自定义控件-文字提示
GitHub:https://github.com/kwwwvagaa/NetWinformControl
冰封一夏
2019/09/09
1.2K0
(五十一)c#Winform自定义控件-文字提示
用 WPF 写的颜色拾取器
之前都是用别人的颜色拾取器,今天自己用WPF写了一个颜色拾取器小程序 拾取鼠标所在位置的颜色,按键盘上的空格键停止取色 程序下载:MyWPFScreenColorE.rar 程序里面有一个全局的勾子
lpxxn
2018/01/31
1.5K0
用 WPF 写的颜色拾取器
【NEW】WPF MVVM 模式下自写自用的窗口样式
SVG是一种图形文件格式,它的英文全称为Scalable Vector Graphics,意思为可缩放的矢量图形。它是基于XML(Extensible Markup Language),由World Wide Web Consortium(W3C)联盟进行开发的。严格来说应该是一种开放标准的矢量图形语言,可让你设计激动人心的、高分辨率的Web图形页面。用户可以直接用代码来描绘图像,可以用任何文字处理工具打开SVG图像,通过改变部分代码来使图像具有交互功能,并可以随时插入到HTML中通过浏览器来观看。
Shunnet
2022/09/01
2.5K0
【NEW】WPF MVVM 模式下自写自用的窗口样式
【C#】分享一个可携带附加消息的增强消息框MessageBoxEx
--------------201806111122更新---------------
AhDung
2018/09/13
2.1K0
【C#】分享一个可携带附加消息的增强消息框MessageBoxEx
基于Python+uiautomation的windowsGUI自动化测试概述
一直使用Python做自动化测试,近期遇到了要对桌面端软件即windowsGUI进行自动化测试。Python的GUI自动化测试工具有很多,但是都有不同的侧重点。本次会详细说明为啥选择uiautomation来做测试。
虫无涯
2023/02/02
4.2K0
【WPF】Toolkit(一个项目)的要点总结
读取系统的显示语言(displayLanguage),显示语言的定义是:假如你的系统现在是中文的,你把它切换到了英文,但是英文的语言包并没有下载下来或者并没有将英文设置为显示语言,那么注销系统再登录之后,你系统显示的将还是中文。此时中文就是显示语言。
全栈程序员站长
2022/11/11
7710
浅析鸿蒙 JavaScript GUI 技术栈
众所周知,刚刚开源的「鸿蒙 2.0」以 JavaScript 作为 IoT 应用开发的框架语言。这标志着继 SpaceX 上天之后,JavaScript 再一次蹭到了新闻联播级的热点。这么好的机会,只拿来阴阳怪气实在太可惜了。作为科普,这篇文章不会拿着放大镜找出代码中的槽点来吹毛求疵,而是希望通俗地讲清楚它所支持的 GUI 到底是怎么一回事。只要对计算机基础有个大概的了解,应该就不会对本文有阅读上的障碍。
zz_jesse
2020/09/17
1.9K0
浅析鸿蒙 JavaScript GUI 技术栈
DirectX 使用 Vortice 从零开始控制台创建 Direct2D1 窗口修改颜色
本文将告诉大家如何使用 Vortice 底层库从零开始,从一个控制台项目,开始搭建一个最简单的使用 Direct2D1 的 DirectX 应用。本文属于入门级博客,期望本文能让大家了解 Vortice 底层库是可以如何调用 DirectX 的功能,以及了解 DirectX 中,特别是 D2D 部分的初始化逻辑
林德熙
2023/04/07
1.3K0
DirectX 使用 Vortice 从零开始控制台创建 Direct2D1 窗口修改颜色
SDL2来源分析3:渲染(SDL_Renderer)
=====================================================
全栈程序员站长
2022/07/06
3.4K0
SDL2来源分析3:渲染(SDL_Renderer)
CImage 类
CImage 提供增强的位图支持,包括加载和保存采用 JPEG、GIF、BMP 和可移植网络图形格式的图像 (PNG) 格式。
全栈程序员站长
2022/09/07
3.6K0
WPF 从裸 Win 32 的 WM_Pointer 消息获取触摸点绘制笔迹
本文将告诉大家如何在 WPF 里面,接收裸 Win 32 的 WM_Pointer 消息,从消息里面获取触摸点信息,使用触摸点信息绘制简单的笔迹
林德熙
2024/09/01
2640
WPF 从裸 Win 32 的 WM_Pointer 消息获取触摸点绘制笔迹
相关推荐
UI自动化 --- 微软UI Automation
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验