Ich habe ein weiteres neues Tool zu meiner Tiny Tools Collection hinzugefügt: ToggleDisplay

Code: https://github.com/sgrottel/tiny-tools-collection/tree/main/ToggleDisplay
Ausführbare Datei: https://github.com/sgrottel/tiny-tools-collection/releases/tag/ToggleDisplay-v1.0

Damit könnt Ihr Displays aktivieren, deaktivieren und umschalten.

Warum das? Mein Computer ist an 2-3 Bildschirme angeschlossen. Zwei Computermonitore auf meinem Schreibtisch für die Arbeit. Und ein Fernseher auf der anderen Seite des Raumes, z.B. um Spiele von meinem Computer aus zu spielen oder um Videodateien in Ruhe anzusehen.

Oft genug fahre ich den Computer hoch, und dann verschwindet meine Maus vom Desktop, weil ich vergessen habe, dass der Fernseher vorher „an“ war, und die Maus hat sich über die Desktop-Monitore hinaus bewegt. Das ist ärgerlich. Die eingebaute Funktion „Windows-Taste + P“ ist verständlicherweise auf zwei Monitore beschränkt. Also musste ich immer „Windows + P“ drücken, dann „Weitere Einstellungen“, warten, bis der Dialog erscheint, herumfummeln, auf „Übernehmen“ drücken, … Ihr versteht, was ich meine.

Also habe ich im Internet recherchiert, wie man eine Anzeige programmgesteuert aktivieren oder deaktivieren kann. Und es gibt mehrere kostenlose Tools, mit denen das möglich ist. Ich habe zwei ausprobiert, und beide haben nicht funktioniert. Dann gibt es einen Hack, bei dem eine ausführbare Datei von Windows 10 unter Windows 11 verwendet wird. Ja, nein. Also gut. Suchen Sie weiter!

Es stellt sich heraus, dass es eine einfache API dafür gibt: ChangeDisplaySettingsEx. Nach einigen Experimenten konnte ich die Anzeige deaktivieren, aber nicht (wieder) aktivieren. Nicht gut genug. Weiter suchen!

Kurz später stellt sich heraus, dass es eine zweite API gibt, die nicht so einfach ist und für die es so gut wie keine brauchbare Dokumentation gibt: SetDisplayConfig. Dies scheint die API zu sein, die der in Windows eingebaute Dialog zur Anzeigekonfiguration verwendet. Aber … wie. Ich fand Code von „PuFF1k“ auf StackOverflow (https://stackoverflow.com/a/62038912/552373), der die API-Aufrufe des Windows-Dialogs zurückentwickelt hat. Ich habe seinen Code ausprobiert, und es funktioniert. Toll! Vielen Dank, PuFF1k!

Im Kern ist der Tricks, keine modeInfo-Daten an SetDisplayConfig zu übergeben, und alle sourceInfo.modeInfoIdx und targetInfo.modeInfoIdx aller Paths auf DISPLAYCONFIG_PATH_MODE_IDX_INVALID zu setzen.

Ein paar Überarbeitungen und Aufräumarbeiten später habe ich ToggleDisplay, bereit, mit der Welt geteilt zu werden.

Übrigens habe ich jetzt auch den Quellcode einiger meiner älteren Tools in dieses Tiny Tools Collection Repository aufgenommen:

Bei dieser Gelegenheit habe ich auch diese Projekte auf die neuesten DotNet-Laufzeiten aktualisiert. Eine automatisierte Build-Pipeline oder Releases habe ich nicht aufgesetzt. Vielleicht ein anderes Mal.

Heute habe ich eine neue Version meines Checkouts-Overview-Tools veröffentlicht.

Version 1.1 bringt neue Funktionen, Verbesserung des Scannens der Festplatten zum Finden neuer Repository-Checkouts und die Möglichkeit beim Update von Entry-Status-Informationen auch ein git fetch durchzuführen.

Ein paar kleinere Verbesserungen der Benutzeroberfläche erzeugen ein konsistenteres Aussehen.

Ladet den neue Release von Github herunter: Release Feature Release v1.1 – Better Disk Scanning and Git Fetch · sgrottel/checkouts-overview (github.com)

Heute stelle ich euch das Tool Checkouts Overview vor.

https://github.com/sgrottel/checkouts-overview
https://go.grottel.net/checkouts-overview

Was? Warum? Weil mir dieses kleine Tool hilft.

In meinem privaten Setup habe ich eine Menge kleinerer Repos ausgecheckt, mit denen ich nur hin und wieder arbeite. Außerdem habe ich mehrere Repos, die einige Textdokumente und deren Historie verwalten. Manche dieser Repos werden mit Servern synchronisiert, die nur gelegentlich online sind, teils um Strom zu sparen, teils aufgrund von VPN- und Netzwerkverbindungsgeschichten. Und so verliere ich oft den Überblick über den Status der Synchronisierung dieser verschiedenen Repos.

Ist alles eingecheckt? — Meistens, ja. Wenn meine Änderungen schon vollständig war.

Ist alles gepusht? — vielleicht.

Sind Branches aktiv? — keine Ahnung.

Ihr braucht diese Anwendung vielleicht nicht, wenn Ihr einen besser strukturierten Arbeitsablauf habt als ich. Meiner ist chaotisch, also brauche ich die Hilfe eines Tools, dieses Tools.

Wenn Ihr interessiert seid, findet Ihr weitere Informationen im Github-Repository der App.

Hinweis: Und was das Icon der App angeht, es geht um (Repository-)Klone, richtig.

Redate ist ein weiteres Werkzeug in meinem wachsenden Werkzeugkasten. Die Idee ist einfach: Viele Programme erzeugen Dateien, schreiben Dateien, aktualisieren Dateien, mit genau demselben Inhalt wie zuvor. Das Datum des letzten Schreibzugriffs der Datei wird natürlich verändert. Der Inhalt bleibt derselbe. Andere Tools wiederum verwenden das Schreibdatum als Hinweis darauf, ob die Dateien geändert wurden. Was ja auch Sinn macht, oder?

Dieses kleine Werkzeug, „Redate“, speichert die MD5-Hashes der Dateien ab, zusammen mit ihrem ursprünglichen Schreibdaten. Wenn das Werkzeug dann erneut auf dieser Liste von Dateien ausgeführt wird, stellt es das ursprüngliche Schreibdatum für alle Dateien mit unveränderten MD5-Werten wieder her. Und das ist es.

Ich verwende es zum Beispiel für Vue.js-Projekte, um die Schreibdaten der Dateien in den dist-Ordnern beizubehalten. Dann kann ein einfacher FTP-Sync nur die geänderten Dateien für das endgültige Deployment übertragen. Dies hilft bei Projekten mit vielen Assets-Dateien die sich nicht verändern.

Ihr könnt Quellcode und Binär-Releases von Github herunterladen.

Leider ist der Eintrag nur auf Amerikanisches Englisch verfügbar. Der Inhalt wird unten in einer verfügbaren Sprache angezeigt. Klicken Sie auf den Link, um die aktuelle Sprache zu ändern.

Yes, I am still using AntTweakBar. As you might know, the development of AntTweakBar is discontinued. At some point in the future, I will switch. Currently, I consider imgui the best successor. But I haven’t had time to look into imgui. So, when I resurrected an old small tool of mine, it still used ATB, and I did not want to recode all of this. But out of „because-I-can,“ I decided  to update all dependencies to their newest versions. As a result the ATB integration with GLFW 3 did not work any longer. A couple of callback functions where changed between GLFW 2 and GLFW 3. I ended up rewriting my glue code between those two libraries.

Here it is, if any of you ever come across the same issue. First the callbacks:

static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
#ifdef HAS_ANTTWEAK_BAR
  if (action == GLFW_PRESS || action == GLFW_REPEAT)
  {
    int twMod = 0;
    bool ctrl;
    if (mods & GLFW_MOD_SHIFT) twMod |= TW_KMOD_SHIFT;
    if (ctrl = (mods & GLFW_MOD_CONTROL)) twMod |= TW_KMOD_CTRL;
    if (mods & GLFW_MOD_ALT) twMod |= TW_KMOD_ALT;

    int twKey = 0;
    switch (key)
    {
    case GLFW_KEY_BACKSPACE: twKey = TW_KEY_BACKSPACE; break;
    case GLFW_KEY_TAB: twKey = TW_KEY_TAB; break;
    //case GLFW_KEY_???: twKey = TW_KEY_CLEAR; break;
    case GLFW_KEY_ENTER: twKey = TW_KEY_RETURN; break;
    case GLFW_KEY_PAUSE: twKey = TW_KEY_PAUSE; break;
    case GLFW_KEY_ESCAPE: twKey = TW_KEY_ESCAPE; break;
    case GLFW_KEY_SPACE: twKey = TW_KEY_SPACE; break;
    case GLFW_KEY_DELETE: twKey = TW_KEY_DELETE; break;
    case GLFW_KEY_UP: twKey = TW_KEY_UP; break;
    case GLFW_KEY_DOWN: twKey = TW_KEY_DOWN; break;
    case GLFW_KEY_RIGHT: twKey = TW_KEY_RIGHT; break;
    case GLFW_KEY_LEFT: twKey = TW_KEY_LEFT; break;
    case GLFW_KEY_INSERT: twKey = TW_KEY_INSERT; break;
    case GLFW_KEY_HOME: twKey = TW_KEY_HOME; break;
    case GLFW_KEY_END: twKey = TW_KEY_END; break;
    case GLFW_KEY_PAGE_UP: twKey = TW_KEY_PAGE_UP; break;
    case GLFW_KEY_PAGE_DOWN: twKey = TW_KEY_PAGE_DOWN; break;
    case GLFW_KEY_F1: twKey = TW_KEY_F1; break;
    case GLFW_KEY_F2: twKey = TW_KEY_F2; break;
    case GLFW_KEY_F3: twKey = TW_KEY_F3; break;
    case GLFW_KEY_F4: twKey = TW_KEY_F4; break;
    case GLFW_KEY_F5: twKey = TW_KEY_F5; break;
    case GLFW_KEY_F6: twKey = TW_KEY_F6; break;
    case GLFW_KEY_F7: twKey = TW_KEY_F7; break;
    case GLFW_KEY_F8: twKey = TW_KEY_F8; break;
    case GLFW_KEY_F9: twKey = TW_KEY_F9; break;
    case GLFW_KEY_F10: twKey = TW_KEY_F10; break;
    case GLFW_KEY_F11: twKey = TW_KEY_F11; break;
    case GLFW_KEY_F12: twKey = TW_KEY_F12; break;
    case GLFW_KEY_F13: twKey = TW_KEY_F13; break;
    case GLFW_KEY_F14: twKey = TW_KEY_F14; break;
    case GLFW_KEY_F15: twKey = TW_KEY_F15; break;
    }
    if (twKey == 0 && ctrl && key < 128)
    {
      twKey = key;
    }
    if (twKey != 0)
    {
      if (::TwKeyPressed(twKey, twMod)) return;
    }
  }
#endif
}

static void charCallback(GLFWwindow* window, unsigned int key)
{
#ifdef HAS_ANTTWEAK_BAR
  if (::TwKeyPressed(key, 0)) return;
#endif
}

static void mousebuttonCallback(GLFWwindow* window, int button, int action, int mods)
{
#ifdef HAS_ANTTWEAK_BAR
  if (::TwEventMouseButtonGLFW(button, action)) return;
#endif
}

static void mousePosCallback(GLFWwindow* window, double xpos, double ypos)
{
#ifdef HAS_ANTTWEAK_BAR
  if (::TwEventMousePosGLFW((int)xpos, (int)ypos)) return;
#endif
}

static void mouseScrollCallback(GLFWwindow* window, double xoffset, double yoffset)
{
#ifdef HAS_ANTTWEAK_BAR
  static double pos = 0;
  pos += yoffset;
  if (::TwEventMouseWheelGLFW((int)pos)) return;
#endif
}

static void resizeCallback(GLFWwindow* window, int width, int height)
{
#ifdef HAS_ANTTWEAK_BAR
  ::TwWindowSize(width, height);
#endif
}

Of course, you can omit the #ifdefs if you don’t care. Add your own codes to the functions after ATB has been handled.

Then, it’s just your typical initialization of GLFW callbacks:

::glfwSetKeyCallback(window, keyCallback);
::glfwSetCharCallback(window, charCallback);
::glfwSetMouseButtonCallback(window, mousebuttonCallback);
::glfwSetCursorPosCallback(window, mousePosCallback);
::glfwSetScrollCallback(window, mouseScrollCallback);
::glfwSetWindowSizeCallback(window, resizeCallback);

Leider ist der Eintrag nur auf Amerikanisches Englisch verfügbar. Der Inhalt wird unten in einer verfügbaren Sprache angezeigt. Klicken Sie auf den Link, um die aktuelle Sprache zu ändern.

HtmlAgilityPackAs can be read on the internet: HtmlAgilityPack is not for beautiful, aka human readable, html files.

“[…] it’s a ‘by design’ choice.” [https://stackoverflow.com/a/5969074]

So everyone redirects you to some other library.

Now, I am a bit stubborn. I want to use HtmlAgilityPack and I want to have indented, human-readable html files. The magic is within text nodes in the DOM. So, I wrote two utility functions to help me out.

First, to get rid of all unwanted whitespaces. This one might be a bit aggressiv, but it was ok for me:

static private void removeWhitespace(HtmlNode node) {
  foreach (HtmlNode n in node.ChildNodes.ToArray()) {
    if (n.NodeType == HtmlNodeType.Text) {
      if (string.IsNullOrWhiteSpace(n.InnerHtml)) {
        node.RemoveChild(n);
      }
    } else removeWhitespace(n);
  }
}

And, second, to create white spaces for line breaks and indentions:

internal static void beautify(HtmlDocument doc) {
  foreach (var topNode in doc.DocumentNode.ChildNodes.ToArray()) {
    switch (topNode.NodeType) {
      case HtmlNodeType.Comment: {
          HtmlCommentNode cn = (HtmlCommentNode)topNode;
          if (string.IsNullOrEmpty(cn.Comment)) continue;
          if (!cn.Comment.EndsWith("\n")) cn.Comment += "\n";
        } break;
      case HtmlNodeType.Element: {
          beautify(topNode, 0);
          topNode.AppendChild(doc.CreateTextNode("\n"));
          //doc.DocumentNode.InsertAfter(doc.CreateTextNode("\n"), topNode);
        } break;
      case HtmlNodeType.Text:
        break;
      default:
        break;
    }
  }
}

private static bool beautify(HtmlNode node, int level) {
  if (!node.HasChildNodes) return false;

  var children = node.ChildNodes.ToArray();
  bool onlyText = true;
  foreach (var c in children) {
    if (c.NodeType != HtmlNodeType.Text) onlyText = false;
  }
  if (onlyText) return false;

  string nli = "\n" + new string('\t', level);

  foreach (var c in children) {
    node.InsertBefore(node.OwnerDocument.CreateTextNode(nli), c);
    if (c.NodeType == HtmlNodeType.Element) {
      if (c.HasChildNodes) {
        if (beautify(c, level + 1)) {
          c.AppendChild(c.OwnerDocument.CreateTextNode(nli));
        }
      }
    }
  }
  return true;
}

As you might see, the code is pretty hacky. But, it works for me. Maybe, it also works for you, or it can be a starting point.

WinForms De-Blurred

Leider ist der Eintrag nur auf Amerikanisches Englisch verfügbar. Der Inhalt wird unten in einer verfügbaren Sprache angezeigt. Klicken Sie auf den Link, um die aktuelle Sprache zu ändern.

The Problem

We have projects working with WinForms GUIS. Different Developers work with different Computers, and these have different DPI settings. Every time the Project is opened on a system with different DPIs the WinForms get scaled, which is painfully bad.

Solution Summary

  • Set in your Forms Designer: Font = MS Sans; 11px
  • In the Forms Ctor, after InitializeComponent, set: Font = SystemFonts.DefaultFont
  • Enable DPI-Awareness, either through a manifest or by API function SetProcessDPIAwareness

Solution Details

mmconf_blurless
Make your WinForms GUI less blurry by being DPI aware

A modern Windows approach to High-DPI Displays

More and more Computers get equipped with High-DPI Displays, a trend I like very much. Pixels cannot be small enough. With modern Windows, however, GUI of older Applications get infamously blurry. This is due to Microsoft’s approach for backward-compatibility: applications will be rendered at their native DPI and scaled up to the system’s DPI. This way, the application does not need to know anything about DPI, but user controls will keep a decent size on any setting. While most people hate the blurriness of old GUIs on modern Windows, this approach does make a lot of sense:

  • Backward-Compatibility is instantly given, as nothing changes for the old Applications.
  • The users input experience is retained, as the GUI will keep its apparent size. (Just thing, if you’re old enough, of the original GUI of Winamp, where the buttons in the default skin were sometimes just a few Pixels in size.)
  • (last but not least) for new Applications, Developers hopefully get upset with the blurry look, that they actually invest some time, to do it right!

So, don’t claim it’s all Microsoft’s fault that the GUI of your applications look blurry. Truth is, you were just too lazy to do it right.

Why WinForms?

If you browse the internet in search for how to handle high-DPI settings with WinForms, you are bound to stumble upon a smart-ass telling you to switch to WPF. That person does have a point: WPF is designed to be a GUI for all resolutions. But, that person is also an idiot. Don’t bother discussing.

If you decided to use WinForms, then use WinForms. It is not deprecated, it is not legacy, it is not broken. There are good reasons to use WinForms. One, for example, would be the nice compatibility with Mono (Linux and MacOS). Another one would be compatibility with native GUI controls. Whatever your reason is, don’t let other people easily throw you off track.

If you’re not fixed on WinForms, but want to write a new Application for Windows, then have a look at WPF.

Why does Visual Studio Scale?

Normally Windows works at 96 DPI. That is, you need 96 pixels to fill up one inch of screen space. On a higher setting, let’s say 144 DPI, you need 144 pixels. So, either your GUI elements will look smaller, or your GUI elements must be larger to look the same. Modern graphical content is thus not described in pixels, but often in points (pt). Points are defined as 1/72 inch, that is in screen space, and not in pixels.

WinForms is not a modern GUI. All Elements are designed with pixels. However, higher DPI settings were present in Windows for a long time (accessibility feature). WinForms answers to this by having a mechanism to scale the whole GUI ‘manually’. If a scaling factor different from one is determined, all GUI elements, positions, sizes, etc., are multiplied by that factor, including the overall size of the window. By default, this scaling factor is determined comparing the Font settings of the form. Fonts are usually specified with a size in pt. Windows computes the font size in pixels based on the active DPI value. If WinForms now detects a mismatch between the pixel-based font sizes between design-time specifications and run-time evaluation, the form and all content is scaled. And this is exactly what happens in the Visual Studio Windows Forms designer.

Visual Studio does basically nothing at all. However, the font size evaluation is based on the system’s DPI setting. So on high-DPI systems, the font’s pixel size will be different from the stored design-time pixel size, and thus the whole form will be scaled accordingly. That is a good idea at run-time. I mean, that is the whole point of this mechanism. However, we are still at design time. The problem raises, because the Forms designer in Visual Studio actually runs the WinForms engine. As now all GUI elements change their sizes, the designer is informed (likely by the normal events) and adjusts all generated code to the new sizes and locations. This is, of course, ugly, painful and stupid, if you are going to continue the development on another machine with another, maybe, lower DPI setting.

Disable Scaling at Design-Time, Enable Scaling at Run-Time

What I am writing here is not a premium solution. It is the workaround I found for myself to work best.

The basic idea is to (manually) disable scaling at design time, and to (manually) enable scaling at run time.

I write scaling, but what we actually change is the Font!

Keep the Form’s AutoScaleMode = Font. That setting is correct and is not the problem at all. The problem is the automatically used font. It is the system’s default font, which specifies its size in pt. Again, a good idea at run time. What we change is the Font setting of the Form, to specify the size in pixels.

In the Designer, set the Font to: Microsoft Sans Serif; 11px

Windows default Font is Microsoft San Serif 8 pt. according to MSDN. Actually, it seems more like 8.25 pt. So this is 8.25/72 inch, which finally results in 8.25 * 96/72 = 11 pixel on a normal DPI system. That is why you set the font to this painfully small looking value. It is the right one! Now edit your GUI on all systems you have. Your Forms will not be scaled by Visual Studio any more. So, design time is fixed.

Now for the run time. That one is easy, too. All we need to do is to ‘reset’ the Form’s font to have its size specified in pt. again. The easiest way to do that is to reassign the system’s default font. Just set it in the constructor, right after InitializeComponent:

Font = SystemFonts.DefaultFont;

This, of course, only works if you are on a system where the system’s default font is as expected, and only if you do not change fonts for the controls inside your form. If you did change some control’s font, you specify the font with pixel size for design time and you update these at run time initialization to use pt.-based sizes again.

Scalable GUI Design

And that is that. If your application is already DPI aware, your forms will now scale nicely. That is, if you designed them correctly.

  • You should not mix docking-based and anchor-based design in the same form. That is bound to produce weird scaling issues.
  • You must use either, otherwise the controls will just randomly shift somewhere.
  • Be aware that the control might no longer fit into your Form, due to the maximum window size. Use flowing layout containers and auto scrollbars.

Enable DPI Awareness

All that, of course, only makes any sense if you enable DPI awareness for your application. There are basically two options:

Manifest-Segment I use:

<application xmlns="urn:schemas-microsoft-com:asm.v3">
  <windowsSettings>
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
  </windowsSettings>
</application>

Code I use:

[DllImport("SHCore.dll")]
private static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness);

private enum PROCESS_DPI_AWARENESS {
    Process_DPI_Unaware = 0,
    Process_System_DPI_Aware = 1,
    Process_Per_Monitor_DPI_Aware = 2
}

[DllImport("user32.dll", SetLastError = true)]
static extern bool SetProcessDPIAware();

internal static void TryEnableDPIAware() {
    try {
        SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware);
    } catch {
        try { // fallback, use (simpler) internal function
            SetProcessDPIAware();
        } catch { }
    }
}

Maybe, you know, that modern Windows can be even more complicated by per-monitor DPI settings. The idea is, that attached external screens have different sizes and resolutions, and should thus be handled with different DPIs. The good news is: this approach here works instantly with per-monitor DPI. When the form is dragged onto another monitor, the system automatically adjusts the font setting, as the font size is at run time specified in points. This automatically triggers a rescaling of the form. Wheee!

Einfache Computergraphik-Demos entwickeln wir gerne als Konsolenanwendungen. Das Konsolenfenster ist einfach praktisch für Debug-Ausgaben. Wenn wir die Programme dann aber auf unserer Stereo-Powerwall zeigen, dann stört das kurz aufblitzende Konsolenfenster massiv. Darum habe ich mal kurz ein kleines Tool zusammengesteckt, dass Konsolenanwendungen startet, die Konsole nicht anzeigt, ihren Inhalt aber abgreift, damit man im Notfall nachsehen kann warum es mal wieder nicht funktioniert.

Ich präsentiere die HiddenConsole:

HiddenConsole.zipHiddenConsole.zip Application starter hiding the console window
[55.3 KB; MD5: 848cbd8aa901fe38be8179d65b6d2162; Mehr Info]

Und weil ich es kann, der Quelltext ist frei verfügbar:

https://bitbucket.org/sgrottel-uni/hiddenconsole

Ich als Visualisierer wissenschaftlicher Datensätze kriege diese häufig in Form von beliebig strukturierten Textdateien. An sich nicht schlimm. Der erste Schritt ist dann eben eine initiale Konvertierung in ein schnelles Binärformat für die visuelle Analyse. Doch damit stehe ich immer wieder vor dem Problem eine 11 Gigabyte Textdatei verstehen zu müssen (Ich übertreibe nicht!). Da die Datei ja doch recht konsistent ist, geht es im Endeffekt immer nur um die ersten paar Zeilen und die letzten paar Zeilen. Dazwischen sieht ja doch meist alles gleich aus. Was ich also brauch sind die üblichen Linux-Befehle „head“ und „tail“. Nun bin ich aber ein Windows-Benutzer. Was nun also? Die Powershell hat die Antwort:

gc log.txt | select -first 10 # head
gc log.txt | select -last 10 # tail

Gefunden hab ich das auf: http://stackoverflow.com/a/9682594 (wo auch sonst)

Zumindest diese Variante von „head“ ist ausreichend schnell für meine Dateien. Ich bin zufrieden.

Heute gibt es mal wieder ein kleines Tool von mir: die ShutdownPlannerGUI

ShutdownPlannerGUI.zipShutdownPlannerGUI.zip Simple GUI for planned Shutdowns of MS Windows
[188 KB; MD5: 45cb64eef13ea47e98a7dcde0773e6f1; Mehr Info]

Die Grundidee ist ganz einfach: es ist eine kleine in C# geschriebene GUI um den Kommandozeilenbefehl Shutdown. Es geht vor allem um die Zeitangabe, wann das System heruntergefahren werden soll. Die GUI bietet mehrere Textfelder um die Zeit bequem in Stunden, Minuten und Sekunden einzugeben. Das ist alles.