/          All Posts       Projects       GitHub       More...
🔗

When comes the night : screen colors inversion

Rien de tel qu’un Ă©cran affichant une page blanche ou claire, pour s’éclater les yeux lors de l’utilisation de son ordinateur dans la pĂ©nombre.
Sans parler des insectes attirĂ©s Ă  100 mĂštres lorsque vous essayez d’aĂ©rer les 28.5°C de votre chambre


Pour atténuer un peu le problÚme, il existe une solution trÚs simple en théorie : inverser les couleurs affichées ! (écran en négatif)

Sous GNU/Linux ou Mac, c’est effectivement simple (Ctrl+Option+Cmd+8 sur Mac, un poil plus compliquĂ© sur Linux)
Mais sur Windows, c’est une autre histoire.

Inventaire de l’existant

options Loupe

Options de Loupe

AprÚs avoir passé des Giga-octets de bande passante en recherches Google, plusieurs solutions se présentent :

– PowerStrip, que j’ai rapidement testĂ© et qui ne semble pas marcher,

– un rĂ©glage au niveau du driver de la carte graphique (des exemples existent pour les cartes Nvidia, mais l’option n’existe apparemment plus dans les drivers les plus rĂ©cents),

– le seul moyen intĂ©grĂ© Ă  Windows : ouvrir la Loupe, (Windows+'+') et activer l’option “Activer l’inversion des couleurs”


Pas top comme systĂšme.

Surtout que l’affichage est saccadĂ©, qu’il faut laisser la Loupe en permanence tourner en arriĂšre plan, ce qui n’est pas trĂšs pratique, et que le gestionnaire de bureau Aero crash rĂ©guliĂšrement quand l’option est activĂ©e.

Bref, aucune des solutions ne me convenait, j’étais frustrĂ©, j’ai codĂ© la mienne.

Creusons un peu

N’ayant que moyennement envie de coder un driver graphique, je me suis tournĂ© vers la solution qui me paraissait la plus simple (et la seule que j’avais vu fonctionner) : imiter la case Ă  cocher des options de la Loupe. (mais en mieux ^^)

AprĂšs quelques essais d’implĂ©mentation naĂŻve en prenant une capture d’écran, en appliquant une transformation pour passer l’image en nĂ©gatif, puis en l’affichant, le tout le plus vite possible, j’ai la confirmation que c’est effectivement une mauvaise idĂ©e. 🙂

Mon processeur ne suit pas, et le framerate non plus. Il va falloir changer de technique


Olly

On peut voir la matrice d'inversion dans la stack... si prĂšs du but...

J’ai alors passĂ© un temps fou Ă  lire de la documentation sur les APIs Windows (l’API Magnification et l’API DWM principalement) pour voir ce qui Ă©tait possible ou non, et ce qui m’attendait.

J’ai dĂ©compilĂ© et Ă©tudiĂ© l’application Loupe elle mĂȘme, espĂ©rant trouver La fonction qui me permettrait simplement d’inverser les couleurs de tout le bureau.

Et j’ai pratiquement rĂ©ussi :

On voit ici deux fonctions intéressantes.

La premiĂšre, SetMagnificationDesktopColorEffect() qui est appelĂ©e par la Loupe lorsque l’on coche ou dĂ©coche la case,

et la seconde, SetMagnificationLensCtxInformation(), qui est appelĂ©e par la premiĂšre fonction. C’est sur cette ligne que le passage en nĂ©gatif de l’écran s’effectue.

Malheureusement, ces deux fonctions ne sont pas documentĂ©es, et je n’ai jamais rĂ©ussi Ă  les appeler avec succĂšs lors de mes essais 😩

Ne laissant pas tomber pour autant,j’ai repris mes recherches, en C# cette fois, en expĂ©rimentant avec les wrappers .Net que Serhiy Perevoznyk a eu la bonne idĂ©e de partager. 🙂

L’exemple donnĂ© est basique, il fonctionne, mais aprĂšs quelques essais, j’ai constatĂ© qu’une fois sur deux, alĂ©atoirement, la zone de dessin de la Loupe restait noire.

j’ai alors regardĂ© du cotĂ© de la documentation officielle, en tentant de modifier les samples C++ du SDK Windows concernant la Loupe.

Le code est plus simple que le wrapper C#, et en modifiant quelques flags, j’arrive enfin Ă  obtenir une image nĂ©gatif !

Mais toujours ce bug alĂ©atoire d’écran noir, prĂ©sent mĂȘme dans les exemples fournis


J’ai mis du temps, mais j’ai fini par tomber sur un forum ou ce mĂȘme bug Ă©tait dĂ©crit et ou une solution Ă©tait donnĂ©e :

Il s’agit bien d’un obscur problĂšme du cotĂ© de Microsoft, et pour le rĂ©soudre, il “suffit” de ne pas utiliser le mode de compatibilitĂ© 32 bits de Windows


Le problĂšme ne survient en effet que dans le cas ou l’application est compilĂ©e en 32 bits, et exĂ©cutĂ©e via WoW64 sur une plateforme 64 bits !

La solution est simple : compiler directement en 64 bits. (ou en 32, pour une plateforme x86)

Un peu de code maintenant

AprĂšs m’ĂȘtre convaincu que ce que je voulais faire Ă©tait faisable, et aprĂšs avoir Ă©cartĂ© les principaux problĂšmes, je suis passĂ© aux choses sĂ©rieuses


J’ai finalement dĂ©cidĂ© de coder en C#, car je maitrise mieux ce langage que le C++, je dispose de wrappers existants, et les performances ne sont pas un problĂšme puisque le principal du travail est gĂ©rĂ© par les fonctions de l’API Magnification.

Il faut se rappeler que mon but est d’inverser les couleurs de l’écran, mais que pour ça, je suis forcĂ© d’utiliser une API pour simuler une Loupe


La manoeuvre est donc un peu plus compliquĂ©e que d’appeler une bĂȘte fonction, comme j’espĂ©rais pouvoir le faire en Ă©tudiant la Loupe au dĂ©compileur.

(J’ai aussi eu l’espoir qu’il existait un quelconque moyen permettant de faire du post processing avant le rendu du bureau (qui est accĂ©lĂ©rĂ© par la carte graphique sous Seven et Vista) mais je n’ai toujours rien trouvĂ© Ă  ce jour 😩)

Pour utiliser la Loupe donc, il faut tout d’abord crĂ©er une fenĂȘtre hĂŽte, puis une fenĂȘtre fille intĂ©grĂ©e Ă  la fenĂȘtre hĂŽte, ayant une classe spĂ©ciale, qui servira de zone de rendu Ă  l’image source grossie.

Il faut ensuite rafraichir cette image grossie en passant par un timer trĂšs rapide, si l’on veut Ă©viter les saccades.

Il est possible d’appliquer une transformation Ă  l’image avant de l’afficher. C’est Ă  cette Ă©tape que l”inversion des couleurs se fait.

L’inversion de couleur Ă©tant apparemment trĂšs utilisĂ©e, il existe mĂȘme un flag (MS_INVERTCOLORS) appliquant cette transformation sans devoir passer par l’application d’une ColorMatrix custom, ce qui m’arrange bien 🙂

class NegativeOverlay : Form
{
	private IntPtr hwndMag;
	private int refreshInterval = 0;

	public NegativeOverlay(int refreshIntervalValue = 0)
		: base()
	{
		this.refreshInterval = refreshIntervalValue;

		this.TopMost = true;
		this.FormBorderStyle = FormBorderStyle.None;
		this.WindowState = FormWindowState.Maximized;
		this.ShowInTaskbar = false;

		//Registers the window class of the magnifier control.
		if (!NativeMethods.MagInitialize())
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}

		IntPtr hInst = NativeMethods.GetModuleHandle(null);
		if (hInst == IntPtr.Zero)
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}

		//apply required styles to host window
		if (NativeMethods.SetWindowLong(this.Handle, NativeMethods.GWL_EXSTYLE, (int)ExtendedWindowStyles.WS_EX_LAYERED | (int)ExtendedWindowStyles.WS_EX_TRANSPARENT) == 0)
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}

		// Make the window opaque
		if (!NativeMethods.SetLayeredWindowAttributes(this.Handle, 0, 255, LayeredWindowAttributeFlags.LWA_ALPHA))
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}

		// Create a magnifier control that fills the client area.
		hwndMag = NativeMethods.CreateWindowEx(0,
			NativeMethods.WC_MAGNIFIER,
			"MagnifierWindow",
			(int)WindowStyles.WS_CHILD |
			(int)WindowStyles.WS_VISIBLE |
			(int)MagnifierStyle.MS_INVERTCOLORS,
			0, 0, Screen.GetBounds(this).Right, Screen.GetBounds(this).Bottom,
			this.Handle, IntPtr.Zero, hInst, IntPtr.Zero);
	}
}

Comme vous pouvez le constater, ça n’est pas le moyen le plus simple ni le plus optimisĂ© auquel on peut penser pour inverser des couleurs


Mais je n’ai pas beaucoup le choix.

Dans notre cas, la zone d’affichage de la Loupe couvrira tout l’écran, il faut donc un moyen pour passer les clicks de souris et les frappes de clavier au travers de cette fenĂȘtre.

Heureusement, cette partie ne pose pas trop de problĂšmes puisqu’il existe un flag dĂ©diĂ© lors de la crĂ©ation d’une fenĂȘtre (WS_EX_TRANSPARENT)

Il faut ensuite songer au rafraichissement de l’image en nĂ©gatif.
L’affichage de l’outil Loupe de Windows est un peu saccadĂ©, mais heureusement, c’est plus un problĂšme de timer que de performances.
AprĂšs quelques essais, j’ai simplement mis le rafraichissement dans une boucle infinie Ă  la suite du constructeur prĂ©cĂ©dent, sans timer.
L’image est donc trĂšs fluide, et bonne surprise, le taux d’utilisation du processeur est tout Ă  fait convenable ! (l’accĂ©lĂ©ration par GPU ne doit pas y ĂȘtre Ă©trangĂšre
)

//show window
this.Show();

bool noError = true;
while (noError)
{
	try
	{
		// Reclaim topmost status, to prevent unmagnified menus from remaining in view. 
		if (!NativeMethods.SetWindowPos(this.Handle, NativeMethods.HWND_TOPMOST, 0, 0, 0, 0,
		(int)SetWindowPosFlags.SWP_NOACTIVATE | (int)SetWindowPosFlags.SWP_NOMOVE | (int)SetWindowPosFlags.SWP_NOSIZE))
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}
		// Force redraw.
		if (!NativeMethods.InvalidateRect(hwndMag, IntPtr.Zero, true))
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}
		//Process Window messages (otherwise, the window appear stalled)
		Application.DoEvents();
	}
	catch (ObjectDisposedException)
	{
		//application is exiting
		noError = false;
		break;
	}
	catch (Exception)
	{
		throw;
	}
}

//cleaning resources...
NativeMethods.MagUninitialize();

ArrivĂ© Ă  ce stade, l’application est presque fonctionelle.
Il manque nĂ©anmoins quelques dĂ©tails gĂȘnants : les effets Aero ne sont pas supportĂ©s correctement. Avec Peek, lors de l’affichage du bureau, l’écran est visible en couleurs normales !
De la mĂȘme façon, lors du Flip 3D (Windows+tab), les miniatures ainsi que le fond d’écran sont encore en couleurs normales.

Il existe heureusement une API permettant de gérer ces problÚmes : DWM (Desktop Window Manager)

bool preventFading = true;
// Prevents a window from fading to a glass sheet when peek is invoked.
// this way, the negative overlay stay always on top
if (NativeMethods.DwmSetWindowAttribute(this.Handle, DWMWINDOWATTRIBUTE.DWMWA_EXCLUDED_FROM_PEEK, ref preventFading, sizeof(int)) != 0)
{
	throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
}
//Exclude the window from Flip3D and display it above the Flip3D rendering.
DWMFLIP3DWINDOWPOLICY threeDPolicy = DWMFLIP3DWINDOWPOLICY.DWMFLIP3D_EXCLUDEABOVE;
if (NativeMethods.DwmSetWindowAttribute(this.Handle, DWMWINDOWATTRIBUTE.DWMWA_FLIP3D_POLICY, ref threeDPolicy, sizeof(int)) != 0)
{
	throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
}

bool disallowPeek = true;
//Do not show peek preview for the window.
if (NativeMethods.DwmSetWindowAttribute(this.Handle, DWMWINDOWATTRIBUTE.DWMWA_DISALLOW_PEEK, ref disallowPeek, sizeof(int)) != 0)
{
	throwMarshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
}

Le but de l’application Ă©tant d’ĂȘtre simple mais fonctionelle, j’ai Ă©galement ajoutĂ© des HotKeys (raccourcis globaux) interceptant certaines combinaisons de touches et effectuant des actions comme quitter immĂ©diatement l’application, dĂ©sactiver temporairement l’inversion des couleurs (trĂšs apprĂ©ciable lorsque l’on arrive sur une page noire Ă  l’origine, et qui ressort donc plus blanc que blanc en nĂ©gatif), etc


//shortcut to toggle inversion : win+alt+N
if (!NativeMethods.RegisterHotKey(this.Handle, TOGGLE_HOTKEY_ID, KeyModifiers.MOD_WIN | KeyModifiers.MOD_ALT, Keys.N))
{
	throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
}
//cleaning:
//NativeMethods.UnregisterHotKey(this.Handle, TOGGLE_HOTKEY_ID);

(Les raccourcis ont Ă©tĂ© choisis pour tenter de ne pas entrer en conflit avec d’autres applications, tout en restant suffisamment accessibles.)

J’ai Ă©galement implĂ©mentĂ© des combinaisons permettant d’augmenter/baisser/remettre Ă  zĂ©ro l’intervalle de rafraichissement de l’image


Conclusion

Je pense avoir fait le tour de ce qu’il y avait à dire sur le sujet.
J’ai tentĂ© de rester simple tout en ajoutant les dĂ©tails qui m’ont intĂ©ressĂ©s au cours de ce projet.

Bugs connus :
– Parfois un lĂ©ger clignotement lors des effets de transition des fenĂȘtres en “TopMost” (tel que le gestionnaire des taches).
ce problĂšme n’est pas prĂ©sent dans l’outil Loupe.
Si vous avez une idĂ©e de ce que l’outil fait de plus que moi pour Ă©viter cela, je suis intĂ©ressĂ©.

L’application semble tout Ă  fait stable, l’ayant utilisĂ© plusieurs heures d’affilĂ©e sans aucun problĂšme.

Si vous avez des questions/remarques n’hĂ©sitez pas Ă  me contacter, ou Ă  laisser un commentaire 🙂

Téléchargement :

Vous pouvez retrouver ce programme complet, ainsi que ses sources, sur cette page !
(English download page here)

Newer Older

Loading comments...