测试篇 c# net7nativeAOT 桌面图标位置备份器

项目简介

备份windows桌面的图标位置到json
项目是 net7 nativeAOT 的框架,内有 json 生成器的处理(为什么强调?因为有坑,结构体需要写个特性,否则会是{}).

编译方式

下载net7框架之后:

在.csproj文件的路径上面输入cmd,回车:
dotnet publish -r win-x64 -c Release

.csproj 文件

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
        <!--aot发布-->
        <PublishAot>true</PublishAot>
    </PropertyGroup>
</Project>

Program.cs

using System.Text.Json;
namespace DesktopBackup;

internal class Program
{
    readonly static string jsonDir = AppDomain.CurrentDomain.BaseDirectory;
    readonly static string jsonFile = "桌面图标位置备份_";

    static void Main(string[] args)
    {
        var ico = new IcoInfo();
        ico.GridAlignTask(() => {
            do
            {
                Console.WriteLine("输入:");
                Console.WriteLine("1: 桌面图标位置打印");
                Console.WriteLine("2: 桌面图标位置备份到json");
                Console.WriteLine("3: 读取最后的json并设置桌面图标位置");
                Console.WriteLine("空格退出");
                var read = Console.ReadKey();

                if (read.KeyChar == '1')
                {
                    Console.WriteLine();
                    var map = ico.GetIcoMap();
                    if (map is null)
                        return;
                    foreach (var item in map)
                        Console.WriteLine($"{item.Key}:{item.Value}");
                }
                else if (read.KeyChar == '2')
                {
                    Console.WriteLine();
                    var map = ico.GetIcoMap();
                    if (map is null)
                        return;
                    // 序列化
                    string json = JsonSerializer.Serialize(map, IcoJson.Context.DictionaryStringIntPoint);
                    File.WriteAllText(GetTimeFile(DateTime.Now), json);
                }
                else if (read.KeyChar == '3')
                {
                    Console.WriteLine();
                    // 反序列化
                    DirectoryInfo dir = new(jsonDir);
                    var jsons = dir.GetFiles("*.json");
                    if (jsons.Length == 0)
                        return;

                    // 获取最后备份
                    DateTime maxTime = DateTime.MinValue;
                    foreach (var item in jsons)
                    {
                        if (!item.Name.Contains(jsonFile))
                            continue;

                        var name = Path.GetFileNameWithoutExtension(item.Name);
                        var index = name.IndexOf(jsonFile);
                        name = name[(index + jsonFile.Length)..];
                        var data = name.Replace("_", "-").Split("-");
                        var time = new DateTime(int.Parse(data[0]),
                                                int.Parse(data[1]),
                                                int.Parse(data[2]),
                                                int.Parse(data[3]),
                                                int.Parse(data[4]),
                                                int.Parse(data[5]));
                        maxTime = time > maxTime ? time : maxTime;
                    }

                    string json = File.ReadAllText(GetTimeFile(maxTime));
                    var map = JsonSerializer.Deserialize(json, IcoJson.Context.DictionaryStringIntPoint);
                    if (map is null)
                        return;
                    ico.SetIcoMap(map);
                }
                else if (read.KeyChar == ' ')
                {
                    break;
                }
            } while (true);
        });
    }

    static string GetTimeFile(DateTime dateTime)
    {
        return jsonDir + jsonFile + dateTime.ToString("yyyy-MM-dd_HH-mm-ss") + ".json";
    }
}

IcoInfo.cs

using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using DictionarySP = System.Collections.Generic.Dictionary<string, DesktopBackup.IntPoint>;

namespace DesktopBackup;

/// <summary>
/// 获得桌面图标名称和位置
/// </summary>
public class IcoInfo : IDisposable
{
    #region Api
    #region user32
    [DllImport("user32.DLL")]
    static extern int SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.DLL")]
    static extern IntPtr FindWindow(string lpszClass, string lpszWindow);
    [DllImport("user32.DLL")]
    static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
    [DllImport("user32.dll")]
    static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint dwProcessId);
    /// <summary>
    /// 打开一个已存在的进程对象,并返回进程的句柄
    /// </summary>
    [DllImport("kernel32.dll")]
    static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
    /// <summary>
    /// 指定进程的虚拟空间保留或提交内存区域,
    /// 除非 flAllocationType 指定 MEM_RESET 参数,否则将该内存区域置0
    /// </summary>
    [DllImport("kernel32.dll")]
    static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
    [DllImport("kernel32.dll")]
    static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint dwFreeType);
    [DllImport("kernel32.dll")]
    static extern bool CloseHandle(IntPtr handle);
    [DllImport("kernel32.dll")]
    static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, IntPtr lpBuffer, int nSize, ref uint vNumberOfBytesRead);
    /// <summary>
    /// 将指定地址范围中的数据
    /// 从指定进程的地址空间复制到当前进程的指定缓冲区
    /// </summary>
    /// <param name="hProcess">进程句柄</param>
    /// <param name="lpBaseAddress">读出数据的地址</param>
    /// <param name="lpBuffer">存放读取数据的地址</param>
    /// <param name="nSize">读出的数据大小</param>
    /// <param name="vNumberOfBytesRead">数据的实际大小</param>
    /// <returns></returns>
    [DllImport("kernel32.dll")]
    static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, IntPtr lpBuffer, int nSize, ref uint vNumberOfBytesRead);
    #endregion

    // https://github.com/AHK-just-me/AHK_Gui_Constants/blob/master/Sources/Const_ListView.ahk
    const int LVM_FIRST = 0x1000;
    const int LVM_GETITEMCOUNT = LVM_FIRST + 4;
    const int LVM_GETITEMW = LVM_FIRST + 75;
    const int LVM_GETITEMPOSITION = LVM_FIRST + 16;
    const int LVM_SETITEMPOSITION = LVM_FIRST + 15;
    const uint PROCESS_VM_OPERATION = 0x0008;
    const uint PROCESS_VM_READ = 0x0010;
    const uint PROCESS_VM_WRITE = 0x0020;
    const uint MEM_COMMIT = 0x1000;
    const uint MEM_RELEASE = 0x8000;
    const uint MEM_RESERVE = 0x2000;
    const uint PAGE_READWRITE = 4;
    const int LVIF_TEXT = 0x0001;

    #region 宏_网格对齐
    const int LVA_SNAPTOGRID = 0x0005;
    const int LVS_EX_SNAPTOGRID = 0x80000;
    const int LVM_GETEXTENDEDLISTVIEWSTYLE = LVM_FIRST + 55;
    const int LVM_SETEXTENDEDLISTVIEWSTYLE = LVM_FIRST + 54;
    static int ListView_GetExtendedListViewStyle(IntPtr AHandle) => SendMessage(AHandle, LVM_GETEXTENDEDLISTVIEWSTYLE, IntPtr.Zero, IntPtr.Zero);
    static int ListView_SetExtendedListViewStyleEx(IntPtr AHandle, int wParam, int lParam) => SendMessage(AHandle, LVM_SETEXTENDEDLISTVIEWSTYLE, (IntPtr)wParam, (IntPtr)lParam);
    static int ListView_SetExtendedListViewStyle(IntPtr AHandle, int exStyle) => SendMessage(AHandle, LVM_SETEXTENDEDLISTVIEWSTYLE, IntPtr.Zero, (IntPtr)exStyle);
    static bool ListView_Arrange(IntPtr AHandle, uint code) => SendMessage(AHandle, code, IntPtr.Zero, IntPtr.Zero) != 0;
    #endregion

    #region 宏_图标信息
    /// <summary>
    /// 节点个数
    /// </summary>
    /// <param name="AHandle">列表视图控件的句柄</param>
    /// <returns></returns>
    static int ListView_GetItemCount(IntPtr AHandle) => SendMessage(AHandle, LVM_GETITEMCOUNT, IntPtr.Zero, IntPtr.Zero);
    /// <summary>
    /// 获取图标位置
    /// </summary>
    /// <param name="AHandle">列表视图控件的句柄</param>
    /// <param name="AIndex">列表视图项的索引</param>
    /// <param name="APoint">坐标</param>
    /// <returns></returns>
    static bool ListView_GetItemPosition(IntPtr AHandle, int AIndex, IntPtr APoint) => SendMessage(AHandle, LVM_GETITEMPOSITION, (IntPtr)AIndex, APoint) != 0;
    /// <summary>
    /// 设置图标位置
    /// </summary>
    /// <param name="AHandle">列表视图控件的句柄</param>
    /// <param name="AIndex">列表视图项的索引</param>
    /// <param name="APoint">坐标</param>
    /// <returns></returns>
    static bool ListView_SetItemPosition(IntPtr AHandle, int AIndex, IntPtr APoint) => SendMessage(AHandle, LVM_SETITEMPOSITION, (IntPtr)AIndex, APoint) != 0;
    #endregion

    /// <summary>
    /// 列表视图结构体
    /// </summary>
    public struct LVITEM
    {
        public int mask;        // 说明此结构中哪些成员是有效的
        public int iItem;       // 项目的索引值(可以视为行号)从0开始
        public int iSubItem;    // 子项的索引值(可以视为列号)从0开始
        public int state;       // 子项的状态
        public int stateMask;   // 状态有效的屏蔽位
        public IntPtr pszText;  // 主项或子项的名称 string
        public int cchTextMax;  // pszText所指向的缓冲区大小
        public int iImage;      // 关联图像列表中指定图像的索引值
        public IntPtr lParam;   // 程序定义的32位参数
        public int iIndent;
        public int iGroupId;
        public int cColumns;
        public IntPtr puColumns;
    }
    #endregion

    #region 构造
    /// <summary>
    ///桌面的 SysListView32 的窗口句柄
    /// </summary>
    readonly IntPtr vHandle;
    /// <summary>
    /// 进程的句柄
    /// </summary>
    readonly IntPtr vProcess;
    /// <summary>
    /// 指定进程虚拟空间的首地址
    /// </summary>
    readonly IntPtr vPointer;
    public IcoInfo()
    {
        // 桌面的 SysListView32 的窗口句柄
        // xp 是 Progman
        // win7 网上说是 "WorkerW"  但是 spy++ 没找到 程序也不正常
        vHandle = FindWindow("Progman", null!);
        vHandle = FindWindowEx(vHandle, IntPtr.Zero, "SHELLDLL_DefView", null!);
        vHandle = FindWindowEx(vHandle, IntPtr.Zero, "SysListView32", null!);
        var flag = GetWindowThreadProcessId(vHandle, out uint vProcessId);
        if (flag == 0)
            throw new ArgumentException($"{nameof(IcoInfo)}进程pid==0");
        vProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, false, vProcessId);
        vPointer = VirtualAllocEx(vProcess, IntPtr.Zero, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    }
    #endregion

    #region 方法
    void Task(Action<int, byte[], uint, LVITEM> action)
    {
        if (action is null)
            return;
        // 图标个数
        int vItemCount = ListView_GetItemCount(vHandle);
        if (vItemCount == 0)
            return;

        try
        {
            const int vBufferNum = 256;
            for (int i = 0; i < vItemCount; i++)
            {
                var vBuffer = new byte[vBufferNum];
                LVITEM lvItem = new()
                {
                    mask = LVIF_TEXT,
                    iItem = i,
                    iSubItem = 0,
                    cchTextMax = vBufferNum,
                    pszText = vPointer + Marshal.SizeOf(typeof(LVITEM))
                };
                uint vNumberOfBytesRead = 0;

                // 分配内存空间
                unsafe
                {
                    WriteProcessMemory(vProcess,
                        vPointer,
                        new IntPtr(&lvItem),
                        Marshal.SizeOf(typeof(LVITEM)),
                        ref vNumberOfBytesRead);
                }

                // 发送信息 获取响应
                _ = SendMessage(vHandle, LVM_GETITEMW, new IntPtr(i), vPointer);
                ReadProcessMemory(vProcess,
                    vPointer + Marshal.SizeOf(typeof(LVITEM)),
                    Marshal.UnsafeAddrOfPinnedArrayElement(vBuffer, 0),
                    vBufferNum,
                    ref vNumberOfBytesRead);

                action.Invoke(i, vBuffer, vNumberOfBytesRead, lvItem);
            }
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 获取(桌面图标名称,坐标)
    /// </summary>
    /// <returns></returns>
    public DictionarySP? GetIcoMap()
    {
        var dict = new DictionarySP();
        Task((i, vBuffer, vNumberOfBytesRead, lvItem) => {
            var name = GetName(vBuffer, vNumberOfBytesRead);
            if (name == "")
            {
                // 文件夹名称改为 .{ED7BA470-8E54-465E-825C-99712043E01C} 会是空的,
                // 而且通过复制可以令它变成两个同名的
                // 此问题不知道怎么解决,放弃算了
                var a = lvItem;
            }

            // 图标坐标
            IntPoint vPoint = new();
            unsafe
            {
                ListView_GetItemPosition(vHandle, i, vPointer);
                _ = ReadProcessMemory(vProcess, vPointer,
                    (IntPtr)(&vPoint),
                    Marshal.SizeOf(vPoint),
                    ref vNumberOfBytesRead);
            }
            // 保存到词典
            if (!dict.ContainsKey(name))
                dict.Add(name, vPoint);
        });
        return dict;
    }

    /// <summary>
    /// 图标名称
    /// </summary>
    /// <param name="vBuffer">缓冲区</param>
    /// <param name="vNumberOfBytesRead">长度</param>
    /// <returns></returns>
    static string GetName(byte[] vBuffer, uint vNumberOfBytesRead)
    {
        var name = Encoding.Unicode.GetString(vBuffer, 0, (int)vNumberOfBytesRead);
        name = name[..name.IndexOf('\0')];
        return name;
    }

    /// <summary>
    /// 设置图标位置
    /// </summary>
    /// <param name="map"></param>
    public void SetIcoMap(DictionarySP map)
    {
        Task((i, vBuffer, vNumberOfBytesRead, lvItem) => {
            var name = GetName(vBuffer, vNumberOfBytesRead);
            if (!map.ContainsKey(name))
                return;
            var vPoint = map[name];
            ListView_SetItemPosition(vHandle, i, MakeLParam(vPoint.X, vPoint.Y));
        });
    }

    // https://www.jianshu.com/p/a5351977b9ee
    // https://bytes.com/topic/mobile-development/answers/867677-listview-messages-how-set-listviewitem-position-lvm_setitemposition
    static IntPtr MakeLParam(int wLow, int wHigh)
    {
        return new IntPtr(((short)wHigh << 16) | (wLow & 0xffff));
    }
    #endregion

    #region 网格对齐
    /// <summary>
    /// 将图标与网格对齐
    /// </summary>
    public bool GridAlign
    {
        get
        {
            var dwExStyle = ListView_GetExtendedListViewStyle(vHandle);
            return (dwExStyle & LVS_EX_SNAPTOGRID) == LVS_EX_SNAPTOGRID;
        }
        set
        {
            // https://blog.csdn.net/hejingdong123/article/details/106299692
            // 这种方式,确实能实现控制 将图标与网格对齐 的打开和关闭,
            // 但是当你在桌面右键鼠标->查看: 发现这里“钩钩”依然还在,其实只是这里没有刷新而已
            var dwExStyle = ListView_GetExtendedListViewStyle(vHandle);
            if (value)
            {
                // 设置 将图标与网格对齐
                if ((dwExStyle & LVS_EX_SNAPTOGRID) != LVS_EX_SNAPTOGRID)
                {
                    ListView_SetExtendedListViewStyleEx(vHandle, LVS_EX_SNAPTOGRID, LVS_EX_SNAPTOGRID);
                    ListView_Arrange(vHandle, LVA_SNAPTOGRID);
                }
            }
            else
            {
                // 取消 将图标与网格对齐
                if ((dwExStyle & LVS_EX_SNAPTOGRID) == LVS_EX_SNAPTOGRID)
                    ListView_SetExtendedListViewStyle(vHandle, dwExStyle & ~LVS_EX_SNAPTOGRID);
            }
        }
    }

    /// <summary>
    /// 取消将图标与网格对齐,执行任务,最后恢复
    /// </summary>
    /// <param name="action"></param>
    public void GridAlignTask(Action action)
    {
        if (GridAlign)
        {
            GridAlign = !GridAlign;
            action?.Invoke();
            GridAlign = !GridAlign;
        }
        else
        {
            action?.Invoke();
        }
    }
    #endregion

    #region IDisposable接口相关函数
    public bool IsDisposed { get; private set; } = false;

    /// <summary>
    /// 手动调用释放
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// 析构函数调用释放
    /// </summary>
    ~IcoInfo()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        // 不重复释放,并设置已经释放
        if (IsDisposed) return;
        IsDisposed = true;

        // 取消本进程地址空间的映射
        if (vProcess == IntPtr.Zero || vPointer == IntPtr.Zero)
            return;
        VirtualFreeEx(vProcess, vPointer, 0, MEM_RELEASE);
        CloseHandle(vProcess);
    }
    #endregion
}

IcoJson.cs

using System.Text.Json.Serialization;
using System.Text.Json;
using System.Diagnostics;
using System.Runtime.InteropServices;
using DictionarySP = System.Collections.Generic.Dictionary<string, DesktopBackup.IntPoint>;

namespace DesktopBackup;

// https://blog.csdn.net/u011527696/article/details/128019229
// https://zhuanlan.zhihu.com/p/579393886?utm_id=0
// 为啥要标记为 partial 因为类的另外部分是 source generator 自动生成的。
[JsonSerializable(typeof(DictionarySP), GenerationMode = JsonSourceGenerationMode.Metadata)]
internal partial class DictionaryPointContext : JsonSerializerContext
{
}

internal class IcoJson
{
    // https://www.cnblogs.com/cdaniu/p/16024229.html
    readonly static JsonSerializerOptions _jsonOptions = new()
    {
        WriteIndented = true,
        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
        //Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(System.Text.Unicode.UnicodeRanges.All),
        //Converters = { new JJBoxConverter<DictionarySP>() }
    };
    internal readonly static DictionaryPointContext Context = new(_jsonOptions);
}


[StructLayout(LayoutKind.Sequential)]
[Serializable]
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public struct IntPoint : IEquatable<IntPoint>
{
    // 要写JsonInclude或访问器,否则json序列化是空值
    [JsonInclude]
    public int X;
    [JsonInclude]
    public int Y;
    public IntPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
    public override bool Equals(object? obj) => obj is IntPoint point && Equals(point);
    public bool Equals(IntPoint other) => X == other.X && Y == other.Y;
    public override int GetHashCode() => HashCode.Combine(X, Y);
    public static bool operator ==(IntPoint? left, IntPoint? right) => left.Equals(right);
    public static bool operator !=(IntPoint? left, IntPoint? right) => !(left == right);
    private string? GetDebuggerDisplay() => ToString();
    public override string ToString() => $"(X = {X}, Y = {Y})";
    public static IntPoint Create(string str)
    {
        var sps = str.Trim('(', ')').Split(",");
        var x = int.Parse(sps[0][(sps[0].IndexOf(":") + 1)..].Trim());//+1是:
        var y = int.Parse(sps[1][(sps[1].IndexOf(":") + 1)..].Trim());//+1是:
        return new(x, y);
    }
}

(完)

posted @ 2022-12-03 22:37  惊惊  阅读(524)  评论(0编辑  收藏  举报