掘金 后端 ( ) • 2024-04-04 13:59

title: Windows 系统级个人文件夹 vs OneDrive简析 date: 2024-04-02 23:34:34 tags: [Windows, OneDrive] categories: Windows excerpt: Documents, Music vs OneDrive

前情提要

众所周知,Windows的系统级个人文件夹包括:Documents, Music, Desktop, Pictures 等等

这些文件夹默认情况下,是在C:\Users\{name}\

那么由于一些妇孺皆知的原因,C盘空间是永远捉襟见肘的

有没有办法将这些文件夹移动到其他驱动器上呢?

Move

伟大的巨硬已经帮我们想到了

打开个人文件夹(如:音乐)的属性-位置标签,我们就可以看到移动按钮

Tip: 不知道大家有没有发现,这个“位置”标签只有这些个人文件夹才有,普通文件夹是没有的

属性

例如选择新位置为:E:\Music,移动,大功告成

有的人可能会说了,直接Ctrl+X剪切不行吗?

听起来有点粗暴,其实经过我的实验,也是可以的,操作系统仍然能够跟踪这些文件夹的位置

:等等,什么叫跟踪,为什么要跟踪

Search

这次我们拿文档Documents)来举例吧

QQ都知道吧,默认情况下,QQ的消息记录默认是保存在“我的文档”下的

QQ个人文件夹

那么问题来了,“我的文档”是可以被移动的(如前文所述),那么QQ如何准确查找“我的文档

总不能是User目录(C:\Users\{name}\) + Documents

// 那也太捞了,不会有人这么写吧,不会吧不会吧

用半月板想都知道,那肯定是有系统API的ya

SHGetKnownFolderPath(更现代) or SHGetFolderPath(前者的包装器)

#include <iostream>
#include <shlobj.h>
#include <knownfolders.h> 

PWSTR path = nullptr;
HRESULT hr = SHGetKnownFolderPath(FOLDERID_Documents, 0, nullptr, &path);
if (hr == S_OK) {
    std::wcout << path << std::endl;
} // 若文件夹不存在(被删除),则Fail

CoTaskMemFree(path);// 释放内存, 无论是否成功
#include <shlobj.h>
// #define CSIDL_MYDOCUMENTS  CSIDL_PERSONAL //  Personal was just a silly name for My Documents

TCHAR szPath[MAX_PATH]; // 文档里说用 MAX_PATH (260)
SHGetFolderPath(nullptr, CSIDL_PERSONAL, nullptr, SHGFP_TYPE_CURRENT, szPath);
// szPath: "E:\Documents"

Qt中,可以用QStandardPaths获取:

QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);

话说这些API是如何知道“我的文档”的当前位置的?

当然是记在注册表啊,见微软文档

“我的文档”文件夹的路径存储在以下注册表项中,其中的 <存储位置的完整路径> 是存储位置的路径:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders

数值名称:Personal 数值类型:REG_SZ 数值数据:存储位置的完整路径

也就是说,Windows会通过注册表来跟踪个人文件夹的当前位置

同时,我们也可以通过查看注册表 来 判断个人文件夹是否移动成功

如果移动后注册表没有更新 或者 “位置”标签消失,说明寄了

坑:编码

这里其实有一个非常大的坑,和Windows API交互经常会有这种问题

就是如何输出TCHAR数组,和PWSTR指针

如果直接

std::cout << szPath << std::end; // TCHAR // SHGetFolderPath
std::wcout << path << std::endl; // PWSTR // SHGetKnownFolderPath

那么估计会死得很惨

看起来用wcout输出PWSTR非常正确(W代表宽字符)

事实也确实如此,但是结果却是:

E:\OneDrive\附件\音乐
E:\OneDrive\

?为什么PWSTR的结果不正确,是SHGetKnownFolderPath出bug了吗,是OneDrive重定向了吗

在阅读了大量文档和Qt源码后(Qt API正确输出,且内部使用了SHGetKnownFolderPath

我发现:居然是打印过程出现了问题!中文没有正常显示(通过调试模式打断点可以看到内存里显示是正常的)

aaaaa,这谁想得到啊,编码不正确不应该是乱码吗,怎么会直接没了!

其实说实话,好好读SHGetKnownFolderPath文档的程序员应该一眼就初见端倪了

The returned path does not include a trailing backslash. For example, "C:\Users" is returned rather than "C:\Users\".

人家都说了,返回的路径不会以'\'结尾的

你看看E:\OneDrive\正常吗?!盯————

为了正常显示中文,需要加入这一行:

setlocale(LC_ALL, ""); // "" == 使用客户环境中缺省的locale ("chs")
// setlocale(LC_ALL, "chs");

然后就正常了

因为默认情况下,locale是"C"(听说是为了可移植性,所以不支持中文)

可通过以下代码获取默认locale

const char* currentLocale = setlocale(LC_ALL, nullptr);
std::cout << "Current locale: " << currentLocale << std::endl; // "C"

你以为这就完了?太年轻了兄弟

TCHAR怎么办呢

难道你想说你看了眼TCAHR的定义:typedef char TCHAR,然后发现很合理,很正常

随便用个coutprintf都能正常输出中文

那你有没有想过TCHART是什么意思,_T有什么用,L有什么用

T可以理解为TEXT,意为文本,也就是会根据UNICODE宏定义自动处理宽窄字符

#ifdef  UNICODE 
typedef WCHAR TCHAR;
#else
typedef char TCHAR;

_T 也是如此

#define _T(x)       __T(x)
#define _TEXT(x)    __T(x)

#define __T(x)      L ## x // if UNICODE
#define __T(x)      x

那么L前缀呢,就是把字面量标记为宽字符

だから,我们需要同时考虑宽窄字符(UNICODE宏是否定义)

#include "tchar.h"

_tprintf(_T("%s\n"), szPath); // TCHAR

可以用_tprintf宏来自动选择printfwprintf// 宏真神奇

// 顺便说一下,Windows API也常会提供两个版本,以A结尾(ANSI,单字节字符)和以W结尾(宽字符),同时还会提供一个宏(不带后缀)来自动选择

啊,我不得不吐槽一下,原生C++的编码太离谱了,不会真有人用得来吧

看看远方的Qt吧,家人们,QString直接就是Unicode编码,舒服

你以为这就完了?仍旧年轻了兄弟

如果你用的是Windows Clion + MSVC,此时你只要:

cout << "测试" << endl;
// 娴嬭瘯

就会得到这一坨,还有一个Waring:

warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止数据丢失

乍一看,文件编码明明就是UTF-8呀,怎么不是Unicode

不会有人不知道UTF-8分为两个版本吧:UTF-8 & UTF-8 with BOM

BOM(byte order mark):用于标记字节序,微软在 UTF-8 中使用 BOM 是因为这样可以把 UTF-8 和 ASCII 等编码明确区分开

两个版本的区别就是:文件开头有没有 U+FEFF

MSVC编译器默认编码是UTF-8 with BOM,如果没有BOMMSVC编译器就不会认为这是Unicode编码,导致编译后打印乱码

Solution:

  • 在IDE的文件编码设置中改为UTF-8 with BOM
  • 或在cmake中强制采用UTF-8编译:add_compile_options(/utf-8)

还有个坑:

Clion正常运行没问题,但是调试模式 还是会乱码,寄

OneDrive

事情到这里就结束了吗,其实才刚刚开始

我发现,当个人文件夹遇上OneDrive,问题就大发了

OneDrive有一个备份个人文件夹的功能

OneDrive备份

如果我们打开某文件夹的备份按钮

那么该文件夹就会被移动到OneDrive文件夹中

这个移动同样会被注册表跟踪,和手动剪切进来没有什么两样

// 有时候你可能会发现,OneDrive自动备份导致文件夹移动后,缺少了“位置”标签页,别慌,重启一下资源管理器就好了

异变

不过,一旦个人文件夹进入了OneDrive,性质就发生了改变

此时如果我们移动这个 个人文件夹(如:音乐)(通过“位置”标签),就会报错

无法移动

如果此时强行用Ctrl+X剪切该文件夹,是可以成功的

但是,该文件夹(如:音乐)就会退化为一个普通文件夹,且名称从"Music"变为了“音乐”

// 原本的“音乐文件夹”虽然看起来叫”音乐“,其实地址栏名称是Music(什么中英混合体)

此时你会发现,注册表中的音乐文件夹依然指向OneDrive\Music

过了一会儿(等资源管理器反应过来),就会在下重新生成一个新的“音乐”文件夹来取代之

// 可能会看到两个一样的“音乐”文件夹,别担心,重启一下资源管理器

这种情况下,你原来的音乐文件夹就废了,只能手动剪切内容了

异变-2

第二种情况,如果你没有手动将OneDrive下的个人文件夹移动走,而是通过刚刚OneDrive的备份开关(关闭备份)

那么,令人震惊的事情发生了,OneDrive中的“音乐”文件夹没有任何变化(甚至图标还在)

C:\Users\{name}\下生成了一个新的“音乐”文件夹(正宫),且内置快捷方式指向OneDrive中的“音乐”文件夹

这波操作,陈独秀你坐下

// 因此,一旦将个人文件夹移动至OneDrive,将很难正常移出去

注释

如果你在OneDrive文件夹内部移动个人文件夹(如:音乐),那么情况将大不相同

情况1:通过“位置”标签页将 OneDrive\Music 移动到 OneDrive\Other\Music

那么神奇的事情将会发生,OneDrive\Music 文件夹依然存在,且内容完好,但是缺少了图标

OneDrive\Other\Music 空有图标(正宫标志),却没有内容

啊这——,分裂了是吧

情况2:直接Ctrl+X剪切

那么一切都很正常

移动OneDrive

你以为这又又完了,太年轻了兄弟

有没有想过,OneDrive也是在C盘的,很占空间的

那假如我要移动OneDrive文件夹,其中的“音乐”文件夹会怎么样?

首先,我们要解决一个问题,要如何移动OneDrive(这玩意儿是个网盘啊,可不是一般的文件夹)

需要参考一下官方文档:更改 OneDrive 文件夹的位置 - Microsoft 支持

  1. 取消此电脑链接
  2. 移动(剪切)OneDrive文件夹
  3. 重新初始化OneDrive,并选择OneDrive文件夹位置(移动后)

好的,那么OneDrive中的Music文件夹会怎么样,能保持正宫位置吗

请看一段广告,我们稍后回来——

好的,没有赞助商,揭晓答案:OneDrive\Music守住了宝座,非常正常,甚至注册表也同步更新了

为什么呢,究竟是被包装在OneDrive中的原因,还是OneDrive取消链接,从网盘退化为普通文件夹的原因!

为了验证,我们先取消OneDrive链接,使其退化为普通文件夹,然后剪切Music文件夹

发现:还是没有卵用,Music文件夹依旧退化

寄,OneDrive文件夹真神奇

Rebuild

如果我们把OneDrive\Music删除会怎么样呢?

此时在user文件夹下生成了一个新的Music文件夹

那么假如我们把这个C:\Users\{name}\Music继续删除呢

emmm,那就没了

此时,SHGetKnownFolderPath的返回Errorhr != S_OK),但是注册表中记录的还是默认位置 //看来会判断是否存在

如何找回呢?

OneDrive备份

如果我们直接打开OneDrive同步与备份中的Music开关

那么OneDrive中会奇迹般地出现“音乐”文件夹,同时注册表也更新了

API

其实我们也可以通过SHGetKnownFolderPath手动新建系统文件夹

只要将dwFlags设置为该值:

KF_FLAG_CREATE
值: 0x00008000
指定强制创建指定文件夹(如果该文件夹尚不存在)。 应用为该文件夹预定义的安全预配。 如果文件夹不存在且无法创建,则该函数将返回失败代码,并且不会返回任何路径
SHGetKnownFolderPath(FOLDERID_Music, KF_FLAG_CREATE, nullptr, &path);

这样,Music文件夹就会在默认位置重新被创建了(Brand-New)!

// 如果想要获取某文件夹的默认存储位置(移动前的)可以这样:

SHGetKnownFolderPath(FOLDERID_Music, KF_FLAG_DONT_VERIFY | KF_FLAG_DEFAULT_PATH, nullptr, &path);
KF_FLAG_DEFAULT_PATH
值: 0x00000400
指定检索已知文件夹的默认路径。 如果未设置此标志,则该函数将检索文件夹的当前路径(可能重定向)。 除非设置了 KF_FLAG_DONT_VERIFY ,否则此标志的执行包括验证文件夹是否存在。

静观其变

既然我们可以通过API去创建个人文件夹,那么同理,其他软件如果需要往这些文件夹写入数据但是发现Not Found,可能大概应该会重新帮我们创建

所以,放几天大概就好了吧,啊哈哈

// 例如:打开网易云音乐就会自动创建

手动新建

或者,我们可以观察一下注册表中对应文件夹的位置,然后手动新建一个一毛一样的文件夹,然后重启资源管理器,大概就会被认为是正宫了

Just没有图标,那么文件夹图标是哪来的呢?

其实是文件夹下的一个隐藏文件(且被系统保护):desktop.ini

显示隐藏文件

对于Music文件夹,desktop.ini内容如下:

[.ShellClassInfo]
LocalizedResourceName=@%SystemRoot%\system32\windows.storage.dll,-21790
InfoTip=@%SystemRoot%\system32\shell32.dll,-12689
IconResource=%SystemRoot%\system32\imageres.dll,-108
IconFile=%SystemRoot%\system32\shell32.dll
IconIndex=-237

第一行设置了文件夹的本地化名称,即在不同语言环境下显示的名称

这也就解释了为什么音乐文件夹显示为"音乐",但实际路径中是"Music"

仔细看可以发现,实际上是从windows.storage.dll中提取ID为21790的资源(字符串)

我们可以验证一下:

HMODULE hModule = LoadLibrary(TEXT("C:\\Windows\\System32\\windows.storage.dll"));
if (hModule == nullptr) {
    std::cerr << "Failed to load DLL." << std::endl;
    return 1;
}

int resourceID = 21790; // 资源标识符

WCHAR buffer[1024];
int length = LoadStringW(hModule, resourceID, buffer, sizeof(buffer) / sizeof(WCHAR));
if (length > 0) {
    std::wcout << L"Resource string: " << buffer << std::endl;
} else {
    std::cerr << "Failed to load string resource." << std::endl;
}
FreeLibrary(hModule); // 释放 DLL 文件

输出为:Resource string: 音乐

  • iconTip设置了当你将鼠标悬停在文件夹图标上时显示的提示信息
  • IconResource是图标资源,和IconFile & IconIndex应该是重复的,大概是为了兼容性考虑

所以其实都是不是很重要的信息,如果我们自己新建Music文件的话,大概可能把这个desktop.ini建出来,外表就大差不大了

疑点

现在还剩下一个最大的疑点,为什么个人文件夹(如:音乐)一旦进入OneDrive,再移动(剪切)出来,就会失去系统属性,堕落为普通文件夹呢

是因为desktop.ini吗?

一般情况下,我们移动文件夹,desktop.ini作为文件夹内的一个文件肯定也会被移动的

不过在OneDrive中的Music比较特殊,如果在文件夹选项中勾选了“隐藏受保护的操作系统文件(推荐)

那么desktop.ini会被隐藏,此时剪切Music文件夹,移出OneDrive,会发现,desktop.ini的内容发生了变化

只剩下更改文件夹名称的这一行了(但其实真是文件名也变成了"音乐")

LocalizedResourceName=@%SystemRoot%\system32\windows.storage.dll,-21790

这就很奇怪了,一般文件夹的剪切应该保留desktop.ini才对

第二种情况,如果我们没有勾选“隐藏受保护的操作系统文件(推荐)”,那就能看到desktop.ini

此时会有几个警告,问你要不要转移和覆盖desktop.ini

会弹出很多次警告,还有一次覆盖4个文件?挺奇怪的(里面总共就俩desktop.ini

然后呢,转移成功,会发现,图标什么的都保留了,desktop.ini也是正确的

但是,一看注册表,哎呀,又fallback到了C:\Users\{name}\下,而且还生成了一个新的正宫

世界十大未解之谜:计算机中的幽灵

Peace

我不干了,等我入职微软再说吧

Ref

SHGetKnownFolderPath 函数 (shlobj_core.h) - Win32 apps | Microsoft Learn

SHGetFolderPathA 函数 (shlobj_core.h) - Win32 apps | Microsoft Learn

“我的文档”文件夹的配置 - Microsoft 支持

Qt获取windows文档、下载、图片等目录路径_qt获取download目录-CSDN博客

c++ - 获取我的文档的路径 - SegmentFault 思否

更改 OneDrive 文件夹的位置 - Microsoft 支持

移动Onedrive文件夹至D盘目录 - 知乎 (zhihu.com)

「带 BOM 的 UTF-8」和「无 BOM 的 UTF-8」有什么区别?网页代码一般使用哪个? - 知乎 (zhihu.com)

ANSI是什么编码?-CSDN博客

解决 C++ printf 汉字问号。含 _tprintf(), printf(), wprintf() 详解_c++汉字变成问号-CSDN博客

[C++] cout、wcout无法正常输出中文字符问题的深入调查(1):各种编译器测试 - zyl910 - 博客园 (cnblogs.com)

更改 OneDrive 文件夹的位置 - Microsoft 支持

移动Onedrive文件夹至D盘目录 - 知乎 (zhihu.com)