测试篇 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);
}
}
(完)