Windows 固定快捷方式到任务栏

目录

前言

一、通过 ShellExecute 配合动词 taskbarpin 固定到任务栏

二、通过 COM 操作模拟点击右键菜单选项卡

三、效仿 Edge 浏览器使用未公开的 COM 接口

四、获取并修改已固定项的列表顺序

五、目前已知的开源解决方案

六、通过 WinRT API 受限控制任务栏和开始菜单

参考文献


本文出处链接:[https://blog.csdn.net/qq_59075481/article/details/139028308]

前言

将 应用程序固定到任务栏 是 Windows 7 中首次引入的一项功能。自其发明以来,微软(根据 Raymond Chen 的说法)就打算不提供(无限的)编程操作。显然,一些开发人员认为他们的应用程序是世界上最棒的,谁不希望我们的应用程序  在安装时>>>  固定到任务栏>>>?!

目前操作任务栏固定的方法有很多种,这里整理常用的几种方法。

一、通过 ShellExecute 配合动词 taskbarpin 固定到任务栏

尽管使用 ShellExecute 并带有动词 taskbarpin(和 SEE_MASK_INVOKEIDLIST 标志集)来固定快捷方式的方法很简单,但是在 Windows 10 已经停止工作。

ShellExecuteW(NULL, L"taskbarpin", shortcut, NULL, NULL, 0); // 固定到任务栏
ShellExecuteW(NULL, L"taskbarunpin", shortcut, NULL, NULL, 0); // 从任务栏解除

在枚举动词之前,微软添加了一项检查来查看调用进程是否是 explorer.exe,并且从应用程序(例如记事本)的“打开文件”对话框中删除“固定到任务栏” 选项。然而,开发人员知道如何解决这个问题伪造 PEB)。

随后,微软在 Windows 10 版本 1809 中进一步完善了检查机制:通过在shell32.dll!CTaskbandPin::v_AllowVerb 中检查进程是否是 explorer.exe 来避免 IID_IContextMenu::InvokeCommand 未检查来源导致的绕过技巧(上文提到的 PEB 伪造)。

只有几种记录在案的操作任务栏固定的方法:

  • 使用组策略设置默认固定的应用程序(用户仍然可以编辑它们)。每次布局文件更新时,都会重新应用这些设置。
  • 使用 ImportStartLayout 或设置包来强制执行固定的应用程序。每次 explorer.exe 启动时都会重新应用这些设置。
  • 使用组策略禁用固定。
  • 使用 Windows.UI.Shell.TaskbarManager 类。这仅适用于应用程序具有程序包标识的情况(将来对 Windows 的更新将允许开发者将标识分配给非 MSIX/APPX 应用程序,这非常方便;你可以构建自己的 RuntimeBroker.exe)。

二、通过 COM 操作模拟点击右键菜单选项卡

这种方法有很多缺陷,比如必须伪装成 explorer.exe (explorer.exe 必须处于正在运行状态)并且模拟在 Shell 的右键菜单中的操作(必须是右键菜单中拥有的选择项),这类操作是容易被中断的,并且实现的兼容性不高(字符串一改或者正在操作右键菜单就失效了)。

注意:右键菜单中的选项会随着系统版本不同而有所变化,最佳的方法是通过 LoadStringW 从 shell32.dll 加载包含特征的字符串资源。下图是 Win11 23H2 目前的字符串 ID(包括固定到任务栏、从任务栏取消固定、固定到开始菜单、从"开始"屏幕取消固定):

我在另外一篇博客简单介绍了 LoadString 获取字符串资源的方法:

获取 Dll 模块的加载字符串资源-CSDN博客

字符串搜索:

测试搜索效果

COM 调用 Shell 右键菜单项的代码 [改进版]: 

  • 增加了最大重试次数 (maxRetries) 和重试间隔 (retryInterval)。原本的 do-while 外循环存在潜在的死循环;
  • 在 for 循环中加入 Sleep(retryInterval),以确保在每次重试之间有一个短暂的等待时间;
  • 在每次循环开始前和结束后都正确释放资源,避免内存泄漏;
  • 一旦成功执行目标项(DoIt)后立即跳出循环。
#include <shldisp.h>
#include <windows.h>

NTSTATUS PinToTaskbar(PCWSTR pFolder, PCWSTR pName)
{
    if (!pFolder || !pName)
        return ERROR_INVALID_PARAMETER;

    const int maxRetries = 5; // 尝试次数
    const int retryInterval = 100; // 重试间隔,单位:毫秒

    // 初始化COM组件
    HRESULT hr = CoInitialize(NULL);
    if (FAILED(hr))
        return HRESULT_CODE(hr);

    // 获取shell的CLSID
    CLSID clsid = { 0 };
    hr = CLSIDFromProgID(L"Shell.Application", &clsid);
    if (FAILED(hr))
    {
        CoUninitialize();
        return HRESULT_CODE(hr);
    }

    BSTR bs = NULL;
    VARIANT var = { VT_BSTR };
    IShellDispatch* pisd = NULL;
    Folder* pf = NULL;
    FolderItem* pfi = NULL;
    FolderItemVerbs* pfivs = NULL;
    FolderItemVerb* pfiv = NULL;

    for (int attempt = 0; attempt < maxRetries; ++attempt)
    {
        // 创建shell实例
        hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IDispatch, (void**)&pisd);
        if (FAILED(hr))
        {
            Sleep(retryInterval);
            continue;
        }

        // 处理文件路径
        var.bstrVal = SysAllocString(pFolder);
        if (!var.bstrVal) 
        { 
            hr = E_OUTOFMEMORY; 
            goto cleanup;
        }
        hr = pisd->NameSpace(var, &pf);
        if (FAILED(hr))
        {
            SysFreeString(var.bstrVal);
            Sleep(retryInterval);
            continue;
        }

        // 处理文件名
        bs = SysAllocString(pName);
        if (!bs) 
        { 
            hr = E_OUTOFMEMORY; 
            SysFreeString(var.bstrVal);
            goto cleanup;
        }
        hr = pf->ParseName(bs, &pfi);
        if (FAILED(hr))
        {
            SysFreeString(bs);
            SysFreeString(var.bstrVal);
            Sleep(retryInterval);
            continue;
        }

        // 获取右键菜单列表
        hr = pfi->Verbs(&pfivs);
        if (FAILED(hr))
        {
            SysFreeString(bs);
            SysFreeString(var.bstrVal);
            Sleep(retryInterval);
            continue;
        }

        long n = 0;
        hr = pfivs->get_Count(&n);
        if (FAILED(hr))
        {
            SysFreeString(bs);
            SysFreeString(var.bstrVal);
            pfivs->Release();
            Sleep(retryInterval);
            continue;
        }

        // 循环遍历右键菜单列表
        BSTR name = NULL;
        BOOL bRet = FALSE;
        for (long i = 0; i < n; i++)
        {
            VARIANT varIndex;
            VariantInit(&varIndex);
            varIndex.vt = VT_I4;
            varIndex.lVal = i;

            hr = pfivs->Item(varIndex, &pfiv);
            if (FAILED(hr)) continue;

            // 对比右键菜单项的名称
            hr = pfiv->get_Name(&name);
            if (SUCCEEDED(hr))
            {
                if (!wcscmp(name, L"固定到任务栏(&K)"))
                {
                    // 执行目标项
                    hr = pfiv->DoIt();
                    SysFreeString(name);
                    pfiv->Release();
                    bRet = TRUE;
                    break;
                }
                SysFreeString(name);
            }
            pfiv->Release();
        }

        if (bRet) break;

        // 释放所用的数据
        if (bs) SysFreeString(bs);
        if (var.bstrVal) SysFreeString(var.bstrVal);
        if (pfivs) pfivs->Release();
        if (pfi) pfi->Release();
        if (pf) pf->Release();
        if (pisd) pisd->Release();

        Sleep(retryInterval);
    }

cleanup:
    // 释放所用的数据
    if (bs) SysFreeString(bs);
    if (var.bstrVal) SysFreeString(var.bstrVal);
    if (pfivs) pfivs->Release();
    if (pfi) pfi->Release();
    if (pf) pf->Release();
    if (pisd) pisd->Release();
    CoUninitialize();

    return HRESULT_CODE(hr);
}

伪装成 explorer 进程(在 Win 10 1809 版本已经失效)

#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
NTSTATUS FakeToExplorer()
{
    // 获取explorer的路径
    WCHAR szwPath[MAX_PATH] = { 0 };
    GetWindowsDirectoryW(szwPath, MAX_PATH);
    wcscat_s(szwPath, L"\\Explorer.exe");
    // 查询当前程序的PEB信息
    PROCESS_BASIC_INFORMATION pbi = { 0 };
    NTSTATUS ret = NtQueryInformationProcess(GetCurrentProcess(),
        ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
    if (!NT_SUCCESS(ret)) return ret;
    // 检查当前程序路径缓冲区是否足够
    USHORT n = (USHORT)wcslen(szwPath);
    if (pbi.PebBaseAddress->ProcessParameters->ImagePathName.MaximumLength / 2 <= n)
        return ERROR_INSUFFICIENT_BUFFER;
    // 伪装成explorer路径,必须要有NULL结尾符
    memcpy(pbi.PebBaseAddress->ProcessParameters->ImagePathName.Buffer, szwPath, n * 2);
    pbi.PebBaseAddress->ProcessParameters->ImagePathName.Buffer[n] = 0; // 结尾符
    pbi.PebBaseAddress->ProcessParameters->ImagePathName.Length = n * 2;
    return ERROR_SUCCESS;
}

目前有效的方法 —— 注入远程线程到 explorer 进程,主要过程如下:

  1. 通过 Findwindow 获取 Progman 窗口句柄,通过 GetWindowThreadProcessId 获取进程 id,再通过 OpenProcess 打开进程句柄
  2. 计算上面实现 COM 操作函数的指令范围,通过偏移量修改 LoadString 的输入参数
  3. 在打开的 explorer 进程句柄中通过 VirtualAllocEx 申请足够的内存空间。
  4. WriteProcessMemory 拷贝 COM 操作函数的指令到申请的内存空间。
  5. CreateRemoteThread 创建远程线程,并将 lpThreadAttributes 参数指向函数的首地址。
  6. 通过 explorer.exe 代为完成 “固定/取消固定” 的操作。

下图是逆向一款桌面美化工具 IconPin.exe 组件,它所采用的方法就是上面的远程线程注入 + COM 模拟操作法。

【补充】

(1)获取当前桌面 Shell 进程 PID 的代码:

DWORD GetCurrentShellProcessId(){
	HWND hProgman = FindWindowW(L"Progman", L"Program Manager");
	if (hProgman == NULL) {
		OutputDebugStringW(L"Find Progman Window failed.\n");
		return 0;
	}


	DWORD dwProcessId = 0;

	GetWindowThreadProcessId(hProgman, &dwProcessId);

	if (dwProcessId == 0) {
		OutputDebugStringW(L"GetWindowThreadProcessId failed.\n");
		return 0;
	}


	return dwProcessId;
}

(2)计算函数返回指令 (ret) 的地址,可以使用 BeaEninge 反汇编库:

#include <iostream>
#include <Windows.h>
#define BEA_USE_STDCALL    // 指明使用stdcall调用约定

extern "C"
{
#include "../IconPinner/beaengine-5.3.0/headers/BeaEngine.h"
#pragma comment(lib, "../IconPinner/beaengine-5.3.0/dll_x64/BeaEngine.lib")
#pragma comment(linker,"/nodefaultlib:crt64.lib")
}

int TestFunction() {
    printf("TestFunc.\n");
    return 1;
}

uint64_t CalcFunctionReturnAddress(LPVOID lpBaseAddress, int dwSize, LPVOID lpVirtualStart)
{
    DISASM Disasm_Info;
    int len;
    uint64_t end_offset = (uint64_t)lpBaseAddress + dwSize;
    uint64_t ret_addr = 0; // 用于记录最近的 ret 指令的地址
    (void)memset(&Disasm_Info, 0, sizeof(DISASM));
    Disasm_Info.EIP = (uint64_t)lpBaseAddress;
    Disasm_Info.Archi = 0x40; // x64 architecture
    Disasm_Info.Options = MasmSyntax;
    Disasm_Info.VirtualAddr = (uint64_t)lpVirtualStart;
    while (!Disasm_Info.Error)
    {
        Disasm_Info.SecurityBlock = (uint64_t)end_offset - Disasm_Info.EIP;
        if (Disasm_Info.SecurityBlock <= 0)
            break;
        len = Disasm(&Disasm_Info);
        switch (Disasm_Info.Error)
        {
        case OUT_OF_BLOCK:
            break;
        case UNKNOWN_OPCODE:
            //printf("%s \n", &Disasm_Info.CompleteInstr);
            Disasm_Info.EIP += 1;
            Disasm_Info.VirtualAddr += 1;
            Disasm_Info.Error = 0;
            break;
        default:
            //printf("0x%I64X |\t%s \n", Disasm_Info.VirtualAddr, &Disasm_Info.CompleteInstr);
            // 检查指令是否包含 "ret"
            if (strstr(Disasm_Info.CompleteInstr, "ret"))
            {
                // 找到最近的 ret 指令,记录其地址并结束循环
                ret_addr = Disasm_Info.VirtualAddr;
                //printf("Function returns at: 0x%p\n", ret_addr);
                return ret_addr;
            }
            Disasm_Info.EIP += len;
            Disasm_Info.VirtualAddr += len;
        }
    }
    printf("No return instruction found.\n");
    return 0;
}

int main(int argc, char* argv[])
{
    void* pBuffer;
    pBuffer = malloc(1000);
    if (pBuffer == nullptr) {
        return 0;
    }

    (void)memcpy(pBuffer, &TestFunction, 1000);

    printf("TestFunc Address: 0x%I64X\n", (uint64_t)&TestFunction);
    
    uint64_t utReturnAddr = CalcFunctionReturnAddress(pBuffer, 1000, &TestFunction);
    printf("Function returns at: 0x%I64X\n", utReturnAddr);
    //system("pause");
    return 0;
}
获取函数返回地址的代码

(3)无模块执行过程调用(无文件注入)

//
//
// FileName : NoModuleInjectProcess.cpp
// Creator : chen516
// Date : 2019/12/18 19:35
// Comment : Inject Process Without Dll File
//
//

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <string>
#include <string.h>
#include <windows.h>
#include <strsafe.h>
#include <tlhelp32.h>

#define MAX_LENGTH 50
#define NORMAL_LENGTH 20

using namespace std;

// 远程线程函数参数
typedef struct _RemoteParam
{
	WCHAR szOperation[NORMAL_LENGTH];
	WCHAR szAddrerss[MAX_LENGTH];
	WCHAR szLb[NORMAL_LENGTH];
	CHAR szFunc[NORMAL_LENGTH];
	LPVOID lpvMLAAdress;
	LPVOID lpvMGPAAddress;
	LPVOID lpvSEAddress;
}RemoteParam, *lpRemoteParam;

// 远程线程函数(主体)
DWORD WINAPI ThreadProc(lpRemoteParam lprp)
{
	typedef HMODULE(WINAPI* MLoadLibraryW)(IN LPCWSTR lpFileName);

	typedef FARPROC(WINAPI* MGetProcAddress)(IN HMODULE hModule, IN LPCSTR lpProcName);

	typedef HINSTANCE(WINAPI* MShellExecuteW)(HWND hwnd, LPCWSTR lpOperation, 
		LPCWSTR lpFile, LPCWSTR lpParameters, LPCWSTR lpDirectory, INT nShowCmd);
	
	MLoadLibraryW m_LoadLibraryW = nullptr;
	MGetProcAddress m_GetProcAddress = nullptr;
	MShellExecuteW m_ShellExecuteW = nullptr;

	m_LoadLibraryW = (MLoadLibraryW)lprp->lpvMLAAdress;
	m_GetProcAddress = (MGetProcAddress)lprp->lpvMGPAAddress;

	HMODULE hMod = m_LoadLibraryW(lprp->szLb);

	if (hMod == nullptr)
	{
		return ERROR_MOD_NOT_FOUND;
	}

	lprp->lpvSEAddress = (LPVOID)m_GetProcAddress(hMod, lprp->szFunc);
	m_ShellExecuteW = (MShellExecuteW)lprp->lpvSEAddress;

	m_ShellExecuteW(NULL, 
		lprp->szOperation, lprp->szAddrerss,
		NULL, NULL, SW_SHOWNORMAL);

	return ERROR_SUCCESS;
}

// 获取 PID
DWORD GetProcessID(PWCHAR ProcessName)
{
	PROCESSENTRY32W pe32 = { 0 };
	pe32.dwSize = sizeof(pe32);
	HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (hProcessSnap == INVALID_HANDLE_VALUE)
	{
		printf("CreateToolhelp32Snapshot error");
		return 0;
	}
	BOOL bProcess = Process32FirstW(hProcessSnap, &pe32);
	while (bProcess)
	{
		if (_wcsicmp(pe32.szExeFile, ProcessName) == 0)  // 转为小写字母后比较
			return pe32.th32ProcessID;
		bProcess = Process32NextW(hProcessSnap, &pe32);
	}
	CloseHandle(hProcessSnap);
	return 0;
}

// 获取权限(有些进程不需要提权)
bool EnableDebugPriv(const TCHAR* privName)
{
	HANDLE hToken = nullptr;
	TOKEN_PRIVILEGES tp = { 0 };
	LUID luid = { 0 };

	if (!OpenProcessToken(GetCurrentProcess(),
		TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
		&hToken))
	{
		printf("OpenProcessToken Error!\n");
		return false;
	}

	if (!LookupPrivilegeValue(NULL, privName, &luid))
	{
		printf("LookupPrivilege Error!\n");
		return false;
	}

	tp.PrivilegeCount = 1;
	tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	tp.Privileges[0].Luid = luid;

	if (!AdjustTokenPrivileges(hToken, 0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL))
	{
		printf("AdjustTokenPrivileges Error!\n");
		return false;
	}

	return true;
}

// 远程线程注入函数
BOOL InjectProcess(const DWORD dwPid)
{
	if (!EnableDebugPriv(SE_DEBUG_NAME)) return FALSE;

	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
	if (!hProcess) return FALSE;

	RemoteParam rp;
	ZeroMemory(&rp, sizeof(RemoteParam));

	const HMODULE hKernel32Mod = LoadLibraryW(L"Kernel32.dll");
	if (!hKernel32Mod)
	{
		return FALSE;
	}

	rp.lpvMLAAdress = (LPVOID)GetProcAddress(hKernel32Mod, "LoadLibraryW");
	rp.lpvMGPAAddress = (LPVOID)GetProcAddress(hKernel32Mod, "GetProcAddress");

	if (rp.lpvMLAAdress == nullptr || rp.lpvMGPAAddress == nullptr) {
		return FALSE;
	}

	StringCchCopyW(rp.szLb, sizeof(rp.szLb) / sizeof(WCHAR), L"Shell32.dll");
	StringCchCopyA(rp.szFunc, sizeof(rp.szFunc) / sizeof(CHAR), "ShellExecuteW");
	StringCchCopyW(rp.szAddrerss, sizeof(rp.szAddrerss) / sizeof(WCHAR), L"https://www.baidu.com");
	StringCchCopyW(rp.szOperation, sizeof(rp.szOperation) / sizeof(WCHAR), L"open");


	RemoteParam* pRemoteParam = (RemoteParam*)VirtualAllocEx(hProcess, 0,
		sizeof(RemoteParam), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (!pRemoteParam) return FALSE;

	if (!WriteProcessMemory(hProcess, pRemoteParam, &rp, sizeof(RemoteParam), 0)) return FALSE;

	LPVOID pRemoteThread = VirtualAllocEx(hProcess, 0, static_cast<SIZE_T>(1024) * 4,
		MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (!pRemoteThread) return FALSE;

	if (!WriteProcessMemory(hProcess, pRemoteThread, &ThreadProc,
		static_cast<SIZE_T>(1024) * 4, 0)) return FALSE;

	HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
		(LPTHREAD_START_ROUTINE)pRemoteThread, 
		(LPVOID)pRemoteParam, 0, NULL);

	if (!hThread) return FALSE;
	return TRUE;
}

// 主函数
int WINAPI main(int argc, TCHAR* argv)
{
	WCHAR szProcName[MAX_LENGTH] = L"\0";
	StringCchCopyW(szProcName, MAX_LENGTH, L"explorer.exe");
	InjectProcess(GetProcessID(szProcName));
	ExitProcess(0);
	return 0;
}

(4)复制代码段和创建远程线程的代码(无模块需要)

...

【补充】有模块注入时,可以采用指向结构体的指针作为参数,将需要传递的参数或者函数链通过结构体传递给远程线程。也可以通过创建进程通信(如命名管道)等来完成调用。

例如下面的就是改成结构体的模块代码片段:

typedef struct tagPINICONINFO{
    PCWSTR pFolder;
    PCWSTR pName; 
    PCWSTR pItemName;
}PINICONINFO, *LPPINICONINFO;


extern "C" __declspec(dllexport)
DWORD
WINAPI
ShellIconPinController(
    LPVOID lpParameters
)
{
    if (!lpParameters)
        return ERROR_INVALID_PARAMETER;

    // 使用 try...except 异常处理包裹的代码段,
    // 是因为线程参数 lpParameters 可能指向无法访问的地址,或者存在错误
    // 防止 ShellIconPinController 读取参数时出现访问冲突。
    __try {
        PINICONINFO* pinfo = (PINICONINFO*)lpParameters;

        const int maxRetries = 5; // 尝试次数
        const int retryInterval = 100; // 重试间隔,单位:毫秒

        // 初始化 COM 组件
        HRESULT hr = CoInitialize(NULL);
        if (FAILED(hr))
            return HRESULT_CODE(hr);

        // 获取 Shell 的 CLSID
        CLSID clsid = { 0 };
        hr = CLSIDFromProgID(L"Shell.Application", &clsid);
        if (FAILED(hr))
        {
            CoUninitialize();
            return HRESULT_CODE(hr);
        }

        BSTR bs = NULL;
        VARIANT var = { VT_BSTR };
        IShellDispatch* pisd = NULL;
        Folder* pf = NULL;
        FolderItem* pfi = NULL;
        FolderItemVerbs* pfivs = NULL;
        FolderItemVerb* pfiv = NULL;

        for (int attempt = 0; attempt < maxRetries; ++attempt)
        {
            // 创建 Shell 实例
            hr = CoCreateInstance(clsid, NULL, 
                CLSCTX_INPROC_SERVER, 
                IID_IDispatch, 
                (void**)&pisd);

            if (FAILED(hr))
            {
                Sleep(retryInterval);
                continue;
            }

            // 处理文件路径
            var.bstrVal = SysAllocString(pinfo->pFolder);
            if (!var.bstrVal)
            {
                hr = E_OUTOFMEMORY;
                goto cleanup;
            }
            hr = pisd->NameSpace(var, &pf);
            if (FAILED(hr))
            {
                SysFreeString(var.bstrVal);
                Sleep(retryInterval);
                continue;
            }

            // 处理文件名
            bs = SysAllocString(pinfo->pName);
            if (!bs)
            {
                hr = E_OUTOFMEMORY;
                SysFreeString(var.bstrVal);
                goto cleanup;
            }
            hr = pf->ParseName(bs, &pfi);
            if (FAILED(hr))
            {
                SysFreeString(bs);
                SysFreeString(var.bstrVal);
                Sleep(retryInterval);
                continue;
            }

            // 获取右键菜单列表
            hr = pfi->Verbs(&pfivs);
            if (FAILED(hr))
            {
                SysFreeString(bs);
                SysFreeString(var.bstrVal);
                Sleep(retryInterval);
                continue;
            }

            long n = 0;
            hr = pfivs->get_Count(&n);
            if (FAILED(hr))
            {
                SysFreeString(bs);
                SysFreeString(var.bstrVal);
                pfivs->Release();
                Sleep(retryInterval);
                continue;
            }

            // 循环遍历右键菜单列表
            BSTR name = NULL;
            BOOL bRet = FALSE;
            for (long i = 0; i < n; i++)
            {
                VARIANT varIndex;
                VariantInit(&varIndex);
                varIndex.vt = VT_I4;
                varIndex.lVal = i;

                hr = pfivs->Item(varIndex, &pfiv);
                if (FAILED(hr)) continue;

                // 对比右键菜单项的名称
                hr = pfiv->get_Name(&name);
                if (SUCCEEDED(hr))
                {
                    if (!wcscmp(name, pinfo->pItemName))   // L"固定到任务栏(&K)"
                    {
                        // 执行目标项
                        hr = pfiv->DoIt();
                        SysFreeString(name);
                        pfiv->Release();
                        bRet = TRUE;
                        break;
                    }
                    SysFreeString(name);
                }
                pfiv->Release();
            }

            if (bRet) break;

            // 释放所用的数据
            if (bs) SysFreeString(bs);
            if (var.bstrVal) SysFreeString(var.bstrVal);
            if (pfivs) pfivs->Release();
            if (pfi) pfi->Release();
            if (pf) pf->Release();
            if (pisd) pisd->Release();

            Sleep(retryInterval);
        }

    cleanup:
        // 释放所用的数据
        if (bs) SysFreeString(bs);
        if (var.bstrVal) SysFreeString(var.bstrVal);
        if (pfivs) pfivs->Release();
        if (pfi) pfi->Release();
        if (pf) pf->Release();
        if (pisd) pisd->Release();
        CoUninitialize();

        return HRESULT_CODE(hr);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        return ERROR_ACCESS_DENIED;
    }
}

那么在调用时候如果结构体的成员是包含指针指向的类型的,则需要先对子结构分配缓冲区和拷贝数据,最后再对上级结构拷贝数据,最终构造出完整的结构体。大体上如下所示:

在远处进程的内存空间内构造结构体变量

(5)获取 Shell 字符串资源并动态匹配名称的代码

运用时对于不同系统版本有所区别,具体如图所示:

Win11 和 Win10 的不同(工具 1.0.0.3 代码片段)

完整项目代码:

1.(有模块注入)[https://download.csdn.net/download/qq_59075481/89487601](更新至:1.0.0.1 版,由于 csdn 上传文件板块技术维护,后续版本的文件暂未通过审核)。

2.(无模块注入)[......]。

目前有模块注入版本已经更新到 1.0.0.5 版本。

本版更新日志:

  1. 修复了代码存在的整数溢出漏洞和句柄泄露漏洞以及潜在的缓冲区溢出问题,添加了必要的边界检查和错误处理;
  2. 新增在操作完成时显示成功完成计数的特性;
  3. 新增一个功能用于强制卸载所有注入的模块,避免在特殊情况下加载了多个不同路径下的模块;
  4. 新增显示 "帮助" 文本的命令行参数;
  5. 增加对命令行输入的文件路径的合法性检查;

1.0.0.4 版更新日志:

  1. 增加了对部分 Win10 系统的兼容,改进了字符串资源的搜索算法,并采用遍历的方法操作所有相似字符串;
  2. 验证了对可执行文件格式的支持;
  3. 优化了部分代码,提高效率;

界面截图如下: 

命令工具帮助页面

编译好的可执行文件持续更新):

1.(有模块注入)[https://pan.baidu.com/s/1kgelhYOrgiHeRcNcX-LV2A?pwd=6666],提取码:6666

2.(无模块注入)[......]。

三、效仿 Edge 浏览器使用未公开的 COM 接口

警告:一切使用未文档化的内部结构和接口完成的扩展功能,可能在未来导致您的程序无法工作。微软可能修改任何细节包括但不限于 COM 接口的迭代更新 / 废弃旧的机制 / 限制作用域。在使用此 COM 代码前,请仔细考虑潜在的缺陷对你所开发软件的影响。本文作者和原作者均不承担责任,此代码仅供学习所用。

Microsoft Edge (Chromium) 可以将网站作为非打包应用程序固定到任务栏,并且可以在 explorer.exe 未启动时就完成处理(现在不可以了,必须在 explorer 正常运行时才能弹出通知),在这过程中使用了未公开的 COM 细节(IPinnedList3 接口),相关接口目前已经被研究者发现并利用。

参数
rclsidCLSID_TaskbanPin
pUnkOuterNULL
dwClsContextCLSCTX_ALL
riid{0dd79ae2-d156-45d4-9eeb-3b549769e940}
ppvrsp + 0x30
返回值S_OK

链接:Microsoft Edge (Chromium) 树立了一个坏的和好的例子:任务栏固定的情况

作者提供的示例代码里面有一个 PIDLFromPath 对象重复析构的错误:

当调用 pinnedList->vtbl->Modify 时,它会接受 PIDLFromPath 对象作为参数,并在函数执行完毕后销毁这个对象。这导致了在 main 函数返回之前就调用了 PIDLFromPath 的析构函数,从而导致了重复释放的问题。

下面予以修正。修正代码中向 Modify 方法传递参数时使用 PIDLFromPath 对象的副本,避免了主函数返回前对同一个对象的重复析构。

使用方法:命令行输入要固定/取消固定的快捷方式( lnk 文件)的绝对路径,后面附加参数 “u” 表示取消固定,否则默认为固定操作。

/*
Copyright (c) 2020 by Gee Law

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
**/

// 包含所需的头文件
#pragma comment(lib, "ole32")
#pragma comment(lib, "shell32")

#define STRICT_TYPED_ITEMIDS
#define WIN32_LEAN_AND_MEAN

#include <iostream>
#include<objbase.h>
#include<shlobj.h>
#include<cstdio>

// 定义用于自动初始化和清理 COM 的结构体
struct CoInitializeGuard
{
    HRESULT const hr;

    // 构造函数用于初始化 COM
    CoInitializeGuard() noexcept : hr(CoInitialize(NULL)) { }

    // 析构函数用于清理 COM
    ~CoInitializeGuard() noexcept
    {
        if (SUCCEEDED(hr))
        {
            CoUninitialize();
        }
    }

    // 将 HRESULT 类型转换为 bool 类型,方便使用
    operator HRESULT() const { return hr; }
};

// 定义从文件路径获取 PIDL 的结构体
struct PIDLFromPath
{
    PIDLIST_ABSOLUTE pidl;

    // 构造函数根据文件路径创建 PIDL
    PIDLFromPath(PCWSTR path) noexcept : pidl(ILCreateFromPathW(path)) { }

    // 析构函数用于释放 PIDL 内存
    ~PIDLFromPath()
    {
        if (pidl)
        {
            ILFree(pidl);
        }
    }

    // 复制构造函数用于创建当前对象的副本
    PIDLFromPath(const PIDLFromPath& other) noexcept
    {
        // 复制 pidl 对象
        pidl = reinterpret_cast<PIDLIST_ABSOLUTE>(ILClone(other.pidl));
    }

    // 移动构造函数用于转移其他对象的 PIDL
    PIDLFromPath(PIDLFromPath&& other) noexcept
    {
        // 移动 pidl 对象
        pidl = other.pidl;
        other.pidl = nullptr; // 避免被其他对象释放
    }

    // 复制赋值运算符用于从其他对象复制 PIDL
    PIDLFromPath&operator=(const PIDLFromPath& other) noexcept
    {
        if (this != &other)
        {
            // 释放当前 pidl 对象
            if (pidl)
            {
                ILFree(pidl);
            }
            // 复制 pidl 对象
            pidl = reinterpret_cast<PIDLIST_ABSOLUTE>(ILClone(other.pidl));
        }
        return *this;
    }

    // 移动赋值运算符用于转移其他对象的 PIDL
    PIDLFromPath& operator=(PIDLFromPath&& other) noexcept
    {
        if (this != &other)
        {
            // 释放当前 pidl 对象
            if (pidl)
            {
                ILFree(pidl);
            }
            // 移动 pidl 对象
            pidl = other.pidl;
            other.pidl = nullptr; // 避免被其他对象释放
        }
        return *this;
    }

    // 将 PIDL 转换为 PIDLIST_ABSOLUTE 类型,方便使用
    operator PIDLIST_ABSOLUTE() const { return pidl; }
};

// 定义用于存储 GUID 的全局变量
const GUID CLSID_TaskbandPin =
{
    0x90aa3a4e, 0x1cba, 0x4233,
    { 0xb8, 0xbb, 0x53, 0x57, 0x73, 0xd4, 0x84, 0x49}
};

const GUID IID_IPinnedList3 =
{
    0x0dd79ae2, 0xd156, 0x45d4,
    { 0x9e, 0xeb, 0x3b, 0x54, 0x97, 0x69, 0xe9, 0x40 }
};

// 定义枚举类型 PLMC
enum PLMC { PLMC_EXPLORER = 4 };

// 定义用于 COM 接口 IPinnedList3 的虚函数表结构体和接口
struct IPinnedList3Vtbl;
struct IPinnedList3 { IPinnedList3Vtbl* vtbl; };

// 定义用于释放 IPinnedList3 的函数指针类型
typedef ULONG STDMETHODCALLTYPE ReleaseFuncPtr(IPinnedList3* that);

// 定义用于修改 IPinnedList3 的函数指针类型
typedef HRESULT STDMETHODCALLTYPE ModifyFuncPtr(IPinnedList3* that,
    PCIDLIST_ABSOLUTE unpin, PCIDLIST_ABSOLUTE pin, PLMC caller);

// 定义 IPinnedList3 的虚函数表结构体
struct IPinnedList3Vtbl
{
    void* QueryInterface;
    void* AddRef;
    ReleaseFuncPtr* Release;
    void* MethodSlot4; void* MethodSlot5; void* MethodSlot6;
    void* MethodSlot7; void* MethodSlot8; void* MethodSlot9;
    void* MethodSlot10; void* MethodSlot11; void* MethodSlot12;
    void* MethodSlot13; void* MethodSlot14; void* MethodSlot15;
    void* MethodSlot16;
    ModifyFuncPtr* Modify;
};

// 定义错误信息字符串
wchar_t const* ERR_STR_USAGE = L"Usage:\n"
"  pin \"C:\\path\\to\\file.lnk\"      Pin a Shortcut.\n"
"  pin \"C:\\path\\to\\file.lnk\" u    Unpin a Shortcut.\n"
"\n"
"Exit codes: -1 = printed usage\n"
"             0 = succeeded\n"
"             1 = CoInitialize failed\n"
"             2 = ILCreateFromPathW failed\n"
"             3 = CoCreateInstance failed\n"
"             4 = IPinnedList3::Modify failed\n";
wchar_t const* ERR_STR_COINIT = L"CoInitialize failed.\n";
wchar_t const* ERR_STR_PIDL = L"ILCreateFromPathW failed.\n";
wchar_t const* ERR_STR_CREATE = L"CoCreateInstance failed.\n";
wchar_t const* ERR_STR_MODIFY = L"IPinnedList3::Modify failed.\n";

// 定义错误码
int const ERR_ID_USAGE = -1, ERR_ID_SUCCEEDED = 0,
ERR_ID_COINIT = 1, ERR_ID_PIDL = 2,
ERR_ID_CREATE = 3, ERR_ID_MODIFY = 4;

int wmain(int argc, const PWSTR* argv)
{
    // 检查参数数量是否正确
    if (argc < 2 || argc > 3)
    {
        fputws(ERR_STR_USAGE, stderr);
        return ERR_ID_USAGE;
    }

    // 检查是否需要固定或取消固定快捷方式
    const bool pinning = (argc != 3 || argv[2][0] != L'u');

    // 初始化 COM 环境
    CoInitializeGuard guard;
    if (!SUCCEEDED(guard))
    {
        fputws(ERR_STR_COINIT, stderr);
        return ERR_ID_COINIT;
    }

    // 根据路径创建 PIDL 对象
    PIDLFromPath pidl(argv[1]);
    if (!(bool)pidl)
    {
        fputws(ERR_STR_PIDL, stderr);
        return ERR_ID_PIDL;
    }

    // 创建 IPinnedList3 实例
    IPinnedList3* pinnedList;
    if (!SUCCEEDED(CoCreateInstance(
        CLSID_TaskbandPin, NULL, CLSCTX_ALL,
        IID_IPinnedList3, (LPVOID*)(&pinnedList))))
    {
        fputws(ERR_STR_CREATE, stderr);
        return ERR_ID_CREATE;
    }

    // 调用 Modify 方法进行固定或取消固定操作
    HRESULT hr = pinnedList->vtbl->Modify(pinnedList,
        pinning ? NULL : pidl,
        pinning ? pidl : NULL,
        PLMC_EXPLORER);

    // 释放对象
    pinnedList->vtbl->Release(pinnedList);

    // 检查操作是否成功
    if (!SUCCEEDED(hr))
    {
        fputws(ERR_STR_MODIFY, stderr);
        return ERR_ID_MODIFY;
    }

    // 操作成功,返回成功状态码
    return ERR_ID_SUCCEEDED;
}

目前,此代码有一个问题,即 Modify 方法是直接返回的(异步)。在 Modify 方法返回前,系统会弹出一个 Toast 应用通知以便于询问用户是否允许固定快捷方式。程序并不知道用户处理弹窗的结果。

只有当点击“是”才会固定到任务栏。所以,当通知设置被禁用或者 “应用” 通知被限制通知,我们就无法操作此选项卡。因此,必须检查通知设置是否正常。

有两种检查方式:一种是官方推荐的通过 ToastNotificationManager 类的 Setting 属性来获知状态(Raymond Chen 在一篇《Old New Thing》博文中详细介绍了这一点),然后使用 Shell 命令打开通知中心设置并指示用户启用通知。

另外一种就是 Github 上有人提供的讨巧方法(Enabling Toast Notifications)。这种方法设置注册表 ToastEnabled 值项为 1 并重启用户应用服务(WpnUserService)。以允许激活全局通知设置(但可能没办法重置针对局部应用的通知设置)。

注意:由于版本更新, WpnUserService 服务后面可能伴随内部版本号标记,例如,WpnUserService_4e3ab 作为新的服务名(此时,原始的 WpnUserService 服务的配置可能没有被彻底删除,但实际上已经没法启动):

真正有效的是下面的这个服务,它的名称随着系统版本而有所不同,所以代码必须枚举所有前缀为 WpnUserService 的服务(对名称恰好为 “WpnUserService” 的需要检查是否有效),并尝试启动 / 重启服务。

根据我的理解,可以通过下面代码实现激活全局通知设置:

// 启用应用通知服务.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <windows.h>
#include <string>
#include <vector>
#include <algorithm>

// 修改注册表项
bool ModifyRegistryKey(HKEY hKey, const std::wstring& subKey,
    const std::wstring& valueName, DWORD value) {
    LONG result;
    HKEY hSubKey;

    // 打开或创建注册表项
    result = RegCreateKeyEx(hKey, subKey.c_str(), 0, nullptr,
        REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hSubKey, nullptr);
    if (result != ERROR_SUCCESS) {
        std::cerr << "Error opening or creating registry key." << std::endl;
        return false;
    }

    // 设置注册表项的值
    result = RegSetValueEx(hSubKey, valueName.c_str(), 0, REG_DWORD,
        reinterpret_cast<BYTE*>(&value), sizeof(value));
    if (result != ERROR_SUCCESS) {
        std::cerr << "Error setting registry value." << std::endl;
        RegCloseKey(hSubKey);
        return false;
    }

    // 关闭注册表项
    RegCloseKey(hSubKey);
    return true;
}

// 获取所有服务名称
std::vector<std::wstring> GetServicesNames() {
    std::vector<std::wstring> serviceNames;
    SC_HANDLE scmHandle = OpenSCManager(nullptr, nullptr, SC_MANAGER_ENUMERATE_SERVICE);
    if (!scmHandle) {
        std::cerr << "Error opening service control manager." << std::endl;
        return serviceNames;
    }

    DWORD bufferSize = 0;
    DWORD serviceCount = 0;
    EnumServicesStatusEx(scmHandle, SC_ENUM_PROCESS_INFO, SERVICE_WIN32,
        SERVICE_STATE_ALL, nullptr, 0, &bufferSize, &serviceCount, nullptr, nullptr);

    std::vector<BYTE> buffer(bufferSize, 0);
    ENUM_SERVICE_STATUS_PROCESS* serviceStatus =
        reinterpret_cast<ENUM_SERVICE_STATUS_PROCESS*>(buffer.data());

    if (!EnumServicesStatusEx(scmHandle, SC_ENUM_PROCESS_INFO, SERVICE_WIN32,
        SERVICE_STATE_ALL, reinterpret_cast<LPBYTE>(serviceStatus),
        bufferSize, &bufferSize, &serviceCount, nullptr, nullptr)) {
        std::cerr << "Error enumerating services." << std::endl;
        CloseServiceHandle(scmHandle);
        return serviceNames;
    }

    for (DWORD i = 0; i < serviceCount; ++i) {
        serviceNames.push_back(serviceStatus[i].lpServiceName);
    }

    CloseServiceHandle(scmHandle);
    return serviceNames;
}

// 获取服务状态
DWORD GetServiceStatus(const std::wstring& serviceName) {
    SC_HANDLE scmHandle = OpenSCManager(nullptr, nullptr, SC_MANAGER_CONNECT);
    if (!scmHandle) {
        std::cerr << "Error opening service control manager." << std::endl;
        return SERVICE_STOPPED;
    }

    SC_HANDLE serviceHandle = OpenService(scmHandle, serviceName.c_str(),
        SERVICE_QUERY_STATUS);
    if (!serviceHandle) {
        std::cerr << "Error opening service." << std::endl;
        CloseServiceHandle(scmHandle);
        return SERVICE_STOPPED;
    }

    SERVICE_STATUS serviceStatus;
    if (!QueryServiceStatus(serviceHandle, &serviceStatus)) {
        std::cerr << "Error querying service status." << std::endl;
        CloseServiceHandle(serviceHandle);
        CloseServiceHandle(scmHandle);
        return SERVICE_STOPPED;
    }

    CloseServiceHandle(serviceHandle);
    CloseServiceHandle(scmHandle);
    return serviceStatus.dwCurrentState;
}

// 启动或重新启动服务
bool StartOrRestartServiceByName(const std::wstring& serviceName) {
    DWORD serviceStatus = GetServiceStatus(serviceName);

    // 如果服务已经启动,则尝试重启服务
    if (serviceStatus == SERVICE_RUNNING || serviceStatus == SERVICE_START_PENDING) {
        SC_HANDLE scmHandle = OpenSCManager(nullptr, nullptr, SC_MANAGER_CONNECT);
        if (!scmHandle) {
            std::cerr << "Error opening service control manager." << std::endl;
            return false;
        }

        std::wcout << L"ServiceName: " << serviceName.c_str() << std::endl;

        SC_HANDLE serviceHandle = OpenService(scmHandle, serviceName.c_str(),
            SERVICE_START | SERVICE_STOP | SERVICE_QUERY_STATUS);
        if (!serviceHandle) {
            std::cerr << "Error opening service." << std::endl;
            CloseServiceHandle(scmHandle);
            return false;
        }

        SERVICE_STATUS serviceStatus;
        if (!ControlService(serviceHandle, SERVICE_CONTROL_STOP, &serviceStatus)) {
            std::cerr << "Error stopping service." << std::endl;
            CloseServiceHandle(serviceHandle);
            CloseServiceHandle(scmHandle);
            return false;
        }

        Sleep(1000); // Wait for a second for the service to stop

        if (!StartService(serviceHandle, 0, nullptr)) {
            std::cerr << "Error starting service." << std::endl;
            CloseServiceHandle(serviceHandle);
            CloseServiceHandle(scmHandle);
            return false;
        }

        CloseServiceHandle(serviceHandle);
        CloseServiceHandle(scmHandle);
        return true;
    }
    // 如果服务没有启动,则启动服务
    else if (serviceStatus == SERVICE_STOPPED || serviceStatus == SERVICE_STOP_PENDING) {
        SC_HANDLE scmHandle = OpenSCManager(nullptr, nullptr, SC_MANAGER_CONNECT);
        if (!scmHandle) {
            std::cerr << "Error opening service control manager." << std::endl;
            return false;
        }

        std::wcout << L"ServiceName: " << serviceName.c_str() << std::endl;

        SC_HANDLE serviceHandle = OpenService(scmHandle, serviceName.c_str(),
            SERVICE_START | SERVICE_QUERY_STATUS);
        if (!serviceHandle) {
            std::cerr << "Error opening service." << std::endl;
            CloseServiceHandle(scmHandle);
            return false;
        }

        if (!StartService(serviceHandle, 0, nullptr)) {
            std::cerr << "Error starting service." << std::endl;
            CloseServiceHandle(serviceHandle);
            CloseServiceHandle(scmHandle);
            return false;
        }

        CloseServiceHandle(serviceHandle);
        CloseServiceHandle(scmHandle);
        return true;
    }

    std::cerr << "Invalid service status." << std::endl;
    return false;
}


int main() {
    // 修改注册表项 ToastEnabled
    if (!ModifyRegistryKey(HKEY_CURRENT_USER,
        L"Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications",
        L"ToastEnabled", 1)) {
        std::cerr << "Failed to modify registry key." << std::endl;
        return 1;
    }

    // 获取所有服务名称
    std::vector<std::wstring> serviceNames = GetServicesNames();

    // 启动所有包含指定字符串的服务
    for (const auto& serviceName : serviceNames) {
        if (serviceName.find(L"WpnUserService") != std::wstring::npos) {
            // 启动或重新启动服务
            if (!StartOrRestartServiceByName(serviceName)) {
                std::cerr << "Failed to start or restart service." << std::endl;
                //return 1;
            }
        }
    }

    std::cout << "Toast notifications enabled successfully." << std::endl;
    //std::cin.get();
    return 0;
}

然而,就像手动设置那样,下面条件必须都满足才能使用户及时观察到弹窗:

而上面的方法不能够获取或控制局部的设置,如对来自 “应用” 的通知设置。

此外,修改并不总是有效,有时候 explorer 会陷入某种特殊的状态而不显示任何通知,即使设置正常。唯一的方法就是检测是否能够正常显示 Toast 通知,然后在失败时尝试重启 explorer 进程:

1. How to detect windows 10 toast notification triggered by another app using a UWP app [LinkHere].

2. Toast notification - How to check if it is visible [LinkHere].

四、获取并修改已固定项的列表顺序

既然我们能够通过多种方法固定/取消固定快捷方式,那么就一定会考虑获取已经固定项的顺序的方法。我查阅了很多资料,资料显示已经固定项同时存在注册表和磁盘存储。

对于磁盘存储,固定项是快捷方式,它们位于下面的路径:

%AppData%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar

这是一个常规的、古老的路径。你可以在这里添加自己的快捷方式,但这是行不通的 —— 因为 explorer 不会更新列表。实际的列表由注册表存储管理,磁盘存储的 lnk 仅仅是图标等信息而已,explorer 在运行时结合注册表、内存结构和 TaskBar 文件夹三者而工作。

对于注册表项,位于注册表如下位置:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband

我们主要观察 Favorites 、FavoritesResolve、FavoritesRemovedChanges 这几项。

Favorites 包含 png 路径字符串,推测和图标有关,FavoritesResolve 和 lnk 快捷方式有关,包含TaskBar 文件夹中的路径;当删除 TaskBar 文件夹中某个 lnk 时重启资源管理器后任务栏中对应项变为无法解析的白色图标,当在重启 explorer 之前删除 FavoritesRemovedChanges 注册表值项,则可以成功移除这个空白项(Remove Chrome pinned items from Start Menu and Taskbar - #6 by stevezeeee)。

所以,一种比较原始的方法就是解析 FavoritesResolve 和 TaskBar 文件夹的信息,然后需要修改顺序时,则通过删除 Taskband 注册表项,并按照理想的顺序重新固定所有图标来完成(Unpin Edge (and pin Internet Explorer) with pure PowerShell)。

删除某个项目,则只要操作前删除 FavoritesRemovedChanges 注册表值项,再删除 TaskBar 文件夹中对应的 lnk ,最后重启 explorer 即可(删除所有 Chrome 快捷方式的 bat 脚本如下来自上面的 #6 by stevezeeee)。

DEL /F /S /Q /A "%AppData%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\*chrome*.lnk"
REG DELETE HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband\ /v FavoritesRemovedChanges /f 
taskkill /f /im explorer.exe && start explorer.exe

但是这种方法不是直接的编程方法,不仅逐一比对解析列表显得十分耗时,而且此方法不支持对 UWP 类应用快捷方式的解析(UWP 应用的快捷方式不在 Taskbar 文件夹中)。在修改顺序时,需要重建整个 Taskband 注册表,这是很糟糕的。就目前情况而言,没有公开资料解释这里的二进制数据格式,尽管它从 Win7 就存在了。

除非研究清楚这里的二进制数据格式,一般地只能老实调用组策略 GPO 并结合 XML 配置文件来完成修改。(在有限版本下可以不使用注册表和组策略,而是可以利用 COM 接口修改任务栏固定项列表,具体见小节三和小节五)

五、目前已知的开源解决方案

我在 Github 上找到了 adamecrAppSwitcherBar (应用切换栏 -- 任务栏增强)项目,使用了 Gee Law 的文章中介绍的 COM 接口来枚举固定到任务栏的快捷方式并计算它们的顺序,该方法支持对 UWP 应用的解析。此外项目还通过 CLSID_StartLayoutCmdlet 接口对开始菜单的快捷方式的获取和布局修改。这是一个很好的例子,但是它是 C# 的,我准备将它移植到 C++。

这里查看 PinsService::GetTaskbarPinnedApplications 代码(链接)。

下文摘录自作者自述文本:

​获取有关固定到任务栏的应用程序的信息似乎有点棘手。最简单的方法是从 AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar 获取 (.lnk) 链接。尝试这种方法时,我很难获得引脚的顺序。固定应用程序的列表也存储在 HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband\ 注册表值 Favorites 中——使用没有记录格式的二进制值。由于注册表值中的信息显然顺序正确,因此我尝试查找链接文件的名称,然后按二进制注册表值中的名称位置对链接(从上述目录检索)进行排序。不幸的是,此方法不适用于 Store/UWP 应用程序。在这种情况下,快速启动文件夹中不会创建任何 .lnk 文件,但我用于获取链接顺序的注册表值包含有关 UWP 应用程序的信息。我尝试以某种方式提取固定商店应用程序的 AppId 以及有关引脚顺序的信息,但我没有找到可靠的方法,只能对已安装的应用程序列表进行非常缓慢的一一检查。所以我放弃了使用这个方法。

我在 Gee Law 的有趣 文章 中发现了本机接口 IPinnedList3 和相关的 COM 类。由于该界面没有记录,并且互联网上几乎没有其他信息来源,我不得不尝试并失败了一点,但最终使其按需要工作。

简化算法如下,真正的实现在 PinsService::GetTaskbarPinnedApplications:

//get the COM class type for {90aa3a4e-1cba-4233-b8bb-535773d48449}
var objType = Type.GetTypeFromCLSID(new Guid(Win32Consts.CLSID_TaskbanPin), false);
//create an instance of COM class
var obj = Activator.CreateInstance(objType);
//get the IPinnedList3 interface
var pinnedList=obj as IPinnedList3;
//get the enumerator
var hrs = pinnedList.EnumObjects(out var iel);
do
{
   hrs = iel.Next(1, out var pidl, out _);
   if (!hrs.IsS_OK) break;

   hrs = Shell32.SHCreateItemFromIDList(pidl, iShellItem2Guid, out var shellItem);
   if (!hrs.IsS_OK || shellItem == null) break;

   //get the information from shell item representing either 
   //link to desktop app or directly Store/UWP app
   ...

   Marshal.FreeCoTaskMem(pidl);
} while (hrs.IsS_OK);

如上所述,该界面不是公开的且没有文档记录(并且也有版本控制),因此它可能会随着某些 Windows 更新而更改。

开始菜单 Pins 解析效果: 

任务栏 Pins 解析效果:

六、通过 WinRT API 受限控制任务栏和开始菜单

(本小节于 2024 / 05 / 20 追加) 

微软提供了 TaskbarManager 类 来控制任务栏快捷方式,但是必须以 SDK 16299 为目标并运行版本 16299 或更高版本才能使用任务栏 API,且访问这个接口需要向微软申请解锁令牌(下面提供的讨论中有人提供了绕过申请直接生成令牌的方法)。

官方的方法(LAF 访问令牌申请表):

绕过技巧: 

几年前我写过如何生成自己的令牌(Generating valid tokens to access Limited Access Features in Windows 10 | Rafael Rivera)。

欢迎您在 https://withinrafael.com/api/windows_laf_token 在线使用我的令牌生成器。 

(功能列表是截至 Windows vNext 25997 的最新功能。无 SLA 。)

虽然这个类支持检查任务栏是否允许固定,当前 APP 是否已经固定,固定或者取消固定,但是根据 Github 上已经使用的开发者所说,目前这个 API 只能访问自己的 APP 而无法操作其他 APP。

链接:Support TaskbarManager (pinning to taskbar) from desktop apps

此外,微软还提供了 StartMenuPrimaryTile API 来控制开始菜单固定项和磁贴的设置,但必须以 SDK 15063 为目标并运行版本 15063 或更高版本才能使用主要磁贴 API。


参考文献

1.用代码实现PIN到任务栏(1) | Study Notes (yhsnlkm.github.io)

2.Microsoft Edge (Chromium) the case of Taskbar pinning

3.c# - How to find Toast notifications are enabled? - Stack Overflow

4.Enabling Toast Notifications · Issue #3 · fox-it/Invoke-CredentialPhisher · GitHub

5.The Old New Thing

6.DesktopToastsSample

7.Remove Chrome pinned items from Start Menu and Taskbar

8.GitHub - adamecr/AppSwitcherBar

9.Windows 7 Taskbar Icons

10.How to PIN MSEdge to taskbar with batch file

11.Unpin Edge (and pin Internet Explorer) with pure PowerShell

12.windows 7 - Taskbar icon for all users

13.How to pin an application to the taskbar

14.How to manage Windows Taskbar Items pinning using Group Policy

15.(any more...)


原文出处链接:https://blog.csdn.net/qq_59075481/article/details/139028308

转载请注明出处。

本文发布于:2024.05.18,更新于:2024.05.20, 2024.06.26-2024.06.29, 2024.07.16.

  • 14
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在Win11系统中,everything无法直接固定任务栏。这是因为Everything是一个第三方软件,而Win11的任务栏主要用于快速访问和启动Windows系统的默认应用程序。然而,虽然无法直接固定任务栏,但我们仍然可以通过其他方法来方便地访问和使用Everything。 首先,我们可以将Everything的快捷方式添加到开始菜单或任务栏上。我们可以右键单击Everything程序的快捷方式,然后选择"固定到开始菜单"或"固定任务栏"选项。这样一来,我们就可以通过点击开始菜单或任务栏上的快捷方式来快速打开Everything。 其次,我们可以使用快捷键来快速启动Everything。我们可以在Everything的快捷方式上右键单击,选择"属性",然后在"快捷键"选项卡中设置一个快捷键,如Ctrl+Alt+E。这样一来,我们只需按下设定的快捷键,就能快速启动Everything。 此外,我们还可以使用Windows搜索功能来搜索和访问Everything。我们只需点击任务栏上的搜索图标,并输入关键词"Everything",系统会自动搜索并显示相关结果。我们可以点击搜索结果中的Everything图标,即可打开并使用该软件。 综上所述,虽然无法直接固定任务栏,但我们可以通过将Everything的快捷方式添加到开始菜单或任务栏,设置快捷键,或使用Windows搜索功能来方便地访问和使用Everything。无论哪种方法都可以帮助我们快速找到并使用这款实用的软件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涟幽516

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值