Fortschritt, .NET-Prüfung, Updates, Fenster merken, Box-Funktionen

- Große Kopien laufen im Hintergrund mit Fortschritt und Abbrechen;
  abgebrochene Kopien werden aufgeräumt, das Original bleibt unverändert
- Sichern/Wiederherstellen mit Fortschritt, keine halben ZIP-Dateien
- Installer auf .NET Framework 4.8: erkennt fehlende .NET 9 Desktop Runtime
  und installiert sie auf Wunsch (Microsoft-Signatur wird geprüft)
- Update-Hinweis über Gitea-Releases, Update mit automatischem Neustart
- Fenstergröße und -position werden gemerkt
- Desktop-Boxen: umbenennen, löschen, verschieben, herausziehen
- Fenster und Boxen kehren auf sichtbare Bildschirme zurück
- README ergänzt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 20:46:48 +02:00
co-authored by Claude Opus 5.5
parent 77bb3e6b9d
commit 1ae6e1abc2
19 changed files with 1098 additions and 95 deletions
+169
View File
@@ -0,0 +1,169 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Win32;
namespace DeskCoreSetup;
/// <summary>Prüft und installiert die von DeskCore benötigte "Microsoft .NET 9 Desktop Runtime".</summary>
public static class Runtime
{
public const string DisplayName = "Microsoft .NET 9 Desktop Runtime";
public const int Major = 9;
public const string DownloadPage = "https://dotnet.microsoft.com/de-de/download/dotnet/9.0";
// offizieller Microsoft-Kurzlink auf die jeweils aktuelle 9.0-Version
private const string InstallerUrl = "https://aka.ms/dotnet/9.0/windowsdesktop-runtime-win-x64.exe";
public static bool Is64BitWindows => Environment.Is64BitOperatingSystem;
public static bool IsInstalled
{
get
{
try
{
var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"dotnet", "shared", "Microsoft.WindowsDesktop.App");
if (Directory.Exists(dir) && Directory.GetDirectories(dir).Any(d => Path.GetFileName(d).StartsWith($"{Major}.")))
return true;
}
catch { }
try
{
using var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
using var k = hklm.OpenSubKey(@"SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App");
if (k?.GetValueNames().Any(n => n.StartsWith($"{Major}.")) == true) return true;
}
catch { }
return false;
}
}
/// <summary>Lädt die Laufzeit von Microsoft und installiert sie (Windows fragt einmal nach Administratorrechten).</summary>
public static void Install(Action<double, string> progress)
{
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
var file = Path.Combine(Path.GetTempPath(), $"windowsdesktop-runtime-{Major}-win-x64.exe");
using (var http = new HttpClient { Timeout = TimeSpan.FromMinutes(20) })
{
http.DefaultRequestHeaders.UserAgent.ParseAdd("DeskCore-Setup");
using var resp = http.GetAsync(InstallerUrl, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult();
resp.EnsureSuccessStatusCode();
long total = resp.Content.Headers.ContentLength ?? 0;
using var input = resp.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
using var output = File.Create(file);
var buffer = new byte[81920];
long done = 0;
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
output.Write(buffer, 0, read);
done += read;
progress(total > 0 ? (double)done / total : 0,
total > 0 ? $"{DisplayName} wird heruntergeladen … {done / 1048576} von {total / 1048576} MB"
: $"{DisplayName} wird heruntergeladen … {done / 1048576} MB");
}
}
// Sicherheit: nur ausführen, wenn die Datei gültig von Microsoft signiert ist
if (!IsSignedByMicrosoft(file))
{
try { File.Delete(file); } catch { }
throw new InvalidOperationException("Die heruntergeladene Datei ist nicht gültig von Microsoft signiert und wurde nicht ausgeführt.");
}
progress(1, $"{DisplayName} wird installiert – bitte die Windows-Abfrage bestätigen …");
Process p;
try
{
p = Process.Start(new ProcessStartInfo(file, "/install /quiet /norestart") { UseShellExecute = true, Verb = "runas" })!;
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
throw new InvalidOperationException($"Die Installation von {DisplayName} wurde abgebrochen.");
}
p.WaitForExit();
try { File.Delete(file); } catch { }
// 0 = ok, 3010/1641 = ok, Neustart empfohlen
if (p.ExitCode is not (0 or 3010 or 1641) || !IsInstalled)
throw new InvalidOperationException($"{DisplayName} konnte nicht installiert werden (Code {p.ExitCode}).");
}
// ---------- Signaturprüfung (WinVerifyTrust) ----------
private static bool IsSignedByMicrosoft(string file)
{
try
{
var subject = X509Certificate.CreateFromSignedFile(file).Subject;
if (subject.IndexOf("O=Microsoft Corporation", StringComparison.OrdinalIgnoreCase) < 0) return false;
}
catch { return false; }
return VerifyTrust(file);
}
private static bool VerifyTrust(string file)
{
var fileInfo = new WINTRUST_FILE_INFO
{
cbStruct = (uint)Marshal.SizeOf<WINTRUST_FILE_INFO>(),
pcwszFilePath = file,
};
var pFile = Marshal.AllocHGlobal(Marshal.SizeOf<WINTRUST_FILE_INFO>());
try
{
Marshal.StructureToPtr(fileInfo, pFile, false);
var data = new WINTRUST_DATA
{
cbStruct = (uint)Marshal.SizeOf<WINTRUST_DATA>(),
dwUIChoice = 2, // WTD_UI_NONE
fdwRevocationChecks = 0, // WTD_REVOKE_NONE
dwUnionChoice = 1, // WTD_CHOICE_FILE
pFile = pFile,
dwStateAction = 0,
dwProvFlags = 0x40, // WTD_SAFER_FLAG
};
var action = new Guid("00AAC56B-CD44-11d0-8CC2-00C04FC295EE"); // WINTRUST_ACTION_GENERIC_VERIFY_V2
return WinVerifyTrust(IntPtr.Zero, ref action, ref data) == 0;
}
finally
{
Marshal.FreeHGlobal(pFile);
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct WINTRUST_FILE_INFO
{
public uint cbStruct;
public string pcwszFilePath;
public IntPtr hFile;
public IntPtr pgKnownSubject;
}
[StructLayout(LayoutKind.Sequential)]
private struct WINTRUST_DATA
{
public uint cbStruct;
public IntPtr pPolicyCallbackData;
public IntPtr pSIPClientData;
public uint dwUIChoice;
public uint fdwRevocationChecks;
public uint dwUnionChoice;
public IntPtr pFile;
public uint dwStateAction;
public IntPtr hWVTStateData;
public IntPtr pwszURLReference;
public uint dwProvFlags;
public uint dwUIContext;
public IntPtr pSignatureSettings;
}
[DllImport("wintrust.dll", ExactSpelling = true, SetLastError = false, CharSet = CharSet.Unicode)]
private static extern int WinVerifyTrust(IntPtr hwnd, ref Guid pgActionID, ref WINTRUST_DATA pWVTData);
}