falahati / WindowsDisplayAPI

WindowsDisplayAPI is a .Net wrapper for Windows Display and Windows CCD APIs
GNU Lesser General Public License v3.0
102 stars 20 forks source link

Display Disable & Enable 'ScreenName' issue #22

Closed Nicks182 closed 1 year ago

Nicks182 commented 1 year ago

Hi

I'm working on a little project that will allow me to store display settings for all 4 of my monitors and then let me restore those settings at a later time.

However, I've run into issues with Display.Enable which calls 'displaySetting.Save' which in turn uses the 'ScreenName' property. From what I can see this won't really work, and is not for me currently, as the 'ScreenName' property won't be consistent when switching a display between on and off. I'm not 100% sure if this is a bug or if I'm just doing it wrong. I've actually had a bit of a hard time getting this far and so it's possible I'm just doing something wrong.

For example, let's say I have 2 displays and both are currently ON. If I call 'WindowsDisplayAPI.Display.GetDisplays()' I will have a list of 2 and the 'ScreenName' for each will be as follows.

\\.\DISPLAY1
\\.\DISPLAY2

Now if I call Disable on the second display, the ScreenName property in the DisplayScreen class will be: \.\DISPLAY2

So far things are working and the second display will be disabled. If I now call 'WindowsDisplayAPI.Display.GetDisplays()' I will have a list of 1 display and it's 'ScreenName' will be: \\.\DISPLAY1

However, I need to call 'WindowsDisplayAPI.UnAttachedDisplay.GetUnAttachedDisplays()' to get the disabled Display I want to turn back on and it's 'ScreenName' is now also: \\.\DISPLAY1

This means that when I call Display.Enable, the 'ScreenName' property in the DisplayScreen class is: \.\DISPLAY1

So now the API is enabling the Display that's already active and not the second display. I am getting the correct display to enable, but because the 'ScreenName' is now the same as the active display's 'ScreenName' the wrong display is being made active due to the use of ScreenName when calling 'displaySetting.Save'. This call can be found at: file: DisplayScreen.cs line: 114

I first thought maybe it's because I'm in a Win10 VM, but I made a WPF demo project to test this and I get the same behavior on my main Windows 10 Desktop.

Link to demo project: https://github.com/Nicks182/WpfDisplayTest

Short video of the WPF demo: https://www.youtube.com/watch?v=2SBjikuNDBU

falahati commented 1 year ago

yeah; you need another way to identify monitors. the target name is not a monitor representation but a logical display from the OS's point of view and might contain one or more actual real display devices connected to it.

take a look at this project: https://github.com/falahati/HeliosDisplayManagement

and this: https://github.com/terrymacdonald/DisplayMagician

Both are created to do precisely what you are trying to do here. the first one uses this library and as for the second one, I don't remember if he decided to move away from this library or still is using it. it is based on the code of the first project so you can probably navigate it similarly if not much is changed. regardless, reading them might be helpful.

Nicks182 commented 1 year ago

I did think it's an OS thing. Already been at this for 2 days after the first library didn't work out and not making much progress trying without a library.

The app I'm making will control audio levels and then also some display settings so I can have it all in one place and stored using LiteDB. This way I can have my audio and displays consistent even after a format.

I will start digging into those 2 projects and see what I can come up with. Thanks for the info.

Regards Nick

Nicks182 commented 1 year ago

Hi

I've been debugging and researching for a good 6 hours and I'm still not sure how to fix this. In the screenshot below you can see that the 'GetDisplayDevices' method is iterating over the display I'm looking for, but because the DeviceKey already exists in the Dictionary it won't get added and so I will never be able to get it.

However, the reason it's already added to the Dictionary is because EnumDisplayDevices will return multiple instances with the same DeviceKey but with a different DeviceName. So then I have 2 device with the same key, but different names?

In the image below DeviceName is red because the previous instance was the same except for the DeviceName.

image

I'm really trying to figure this out, but most of this stuff is beyond me and I wasn't able to get any wiser trying to look through the source code of DisplayMagician.

I'm taking part in a little dev competition in which you challenge yourself do work on something you haven't before and I'm running out of time. How it works isn't as important as that it actually does. I'm already not reaching one of my other goals so I kinda want this one.

If you have any ideas on what I could try/change I would really appreciate it. Asking here is pretty much my last resort.

falahati commented 1 year ago

Ok, I will try to point you in the right direction.

first, don't use the display context API. it is an old API used mainly with Windows XP and below and lacks many features. The problem you have encountered is exactly due to the fact that this old API does not support new functionalities provided by Windows.

What you need to do is to use the DisplayConfig namespace to manage your screens. So this alone should solve the majority of your problems.

Now to use this namespace, you have to consider the following naming:

Each display source might contain one or more display targets. Meaning that in a clone configuration with two devices showing the same output, there will be one display source, but two display targets.

Now what you have to do is to create a graph of these nodes. This is done by the PathInfo and PathTargetInfo. A path defines how each display source is connected to the child targets and other information that represents the view with each target having its own configuration about specific settings that applies to that specific device. The currently active list of paths can be retrieved by calling the PathInfo.GetActivePaths() method.

as an example, these three classes are used to store the topology of the display config API in a custom JSON structure and then back again to a valid PathInfo and PathTargetInfo: https://github.com/falahati/HeliosDisplayManagement/tree/master/HeliosDisplayManagement.Shared/Topology

as is obvious from the code, we try to find a display device using the PathDisplayTarget.GetDisplayTargets() API and then filter the result by the device's path to locate the actual device needed. This should be almost always reliable. However, sometimes this technic fails with very similar devices and you might have to rely on the serial of the device extracted from EDID. this part is not implemented in the code provided tho and honestly fails a lot too since manufacturers are lazy and might not actually provide the monitor's serial number in the embedded EDID. don't worry about this part too much. the provided implementation must be enough for the task at hand.

The resulting PathInfo can then be applied using the PathInfo.ApplyPathInfos() method. In fact, the above 3 classes along with the following class are enough to do what you want to achieve here almost completely: https://github.com/falahati/HeliosDisplayManagement/blob/master/HeliosDisplayManagement.Shared/Profile.cs

All you have to do is to remove any code related to NVIDIA's mosaic topology and include missing enum types and you should be good to go.

have a nice day.

falahati commented 1 year ago

the second link was incorrect. fixed in an edit.

I am going to close this issue since I don't think I can provide more information about this. but feel free to open a new issue if you had any other specific issue with the library or help with how an specific part works.