前情提要
众所周知,Windows
的系统级个人文件夹包括:Documents
, Music
, Desktop
, Pictures
等等
这些文件夹默认情况下,是在C:\Users\{name}\
下
那么由于一些妇孺皆知的原因,C盘空间是永远捉襟见肘的
有没有办法将这些文件夹移动到其他驱动器上呢?
Move
伟大的巨硬已经帮我们想到了
打开个人文件夹(如:音乐)的属性-位置标签,我们就可以看到移动按钮
Tip: 不知道大家有没有发现,这个"位置"标签只有这些个人文件夹才有,普通文件夹是没有的
例如选择新位置为:E:\Music,移动,大功告成
有的人可能会说了,直接Ctrl+X
剪切不行吗?
听起来有点粗暴,其实经过我的实验,也是可以的,操作系统仍然能够跟踪这些文件夹的位置
:等等,什么叫跟踪,为什么要跟踪
Search
这次我们拿文档 (Documents
)来举例吧
QQ
都知道吧,默认情况下,QQ
的消息记录默认是保存在"我的文档"下的
那么问题来了,"我的文档 "是可以被移动的(如前文所述),那么QQ
如何准确查找"我的文档"
总不能是User
目录(C:\Users\{name}\
) + Documents
吧
// 那也太捞了,不会有人这么写吧,不会吧不会吧
用半月板想都知道,那肯定是有系统API
的ya
SHGetKnownFolderPath
(更现代) or SHGetFolderPath
(前者的包装器)
cpp
#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);// 释放内存, 无论是否成功
cpp
#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
获取:
cpp
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
话说这些API
是如何知道"我的文档"的当前位置的?
当然是记在注册表啊,见微软文档
"我的文档"文件夹的路径存储在以下注册表项中,其中的 <存储位置的完整路径> 是存储位置的路径:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders
数值名称:Personal 数值类型:REG_SZ 数值数据:存储位置的完整路径
也就是说,Windows
会通过注册表来跟踪个人文件夹的当前位置
同时,我们也可以通过查看注册表 来 判断个人文件夹是否移动成功
如果移动后注册表没有更新 或者 "位置"标签消失,说明寄了
坑:编码
这里其实有一个非常大的坑,和Windows API
交互经常会有这种问题
就是如何输出TCHAR
数组,和PWSTR
指针
如果直接
cpp
std::cout << szPath << std::end; // TCHAR // SHGetFolderPath
std::wcout << path << std::endl; // PWSTR // SHGetKnownFolderPath
那么估计会死得很惨
看起来用wcout
输出PWSTR
非常正确(W代表宽字符)
事实也确实如此,但是结果却是:
makefile
E:\OneDrive\附件\音乐
E:\OneDrive\
?为什么PWSTR
的结果不正确,是SHGetKnownFolderPath
出bug了吗,是OneDrive重定向了吗
在阅读了大量文档和Qt源码后(Qt API正确输出,且内部使用了SHGetKnownFolderPath
)
我发现:居然是打印过程出现了问题!中文没有正常显示(通过调试模式打断点可以看到内存里显示是正常的)
aaaaa,这谁想得到啊,编码不正确不应该是乱码吗,怎么会直接没了!
其实说实话,好好读SHGetKnownFolderPath文档的程序员应该一眼就初见端倪了
sql
The returned path does not include a trailing backslash. For example, "C:\Users" is returned rather than "C:\Users\".
人家都说了,返回的路径不会以'\'
结尾的
你看看E:\OneDrive\
正常吗?!盯------------
为了正常显示中文,需要加入这一行:
cpp
setlocale(LC_ALL, ""); // "" == 使用客户环境中缺省的locale ("chs")
// setlocale(LC_ALL, "chs");
然后就正常了
因为默认情况下,locale
是"C"(听说是为了可移植性,所以不支持中文)
可通过以下代码获取默认locale
:
cpp
const char* currentLocale = setlocale(LC_ALL, nullptr);
std::cout << "Current locale: " << currentLocale << std::endl; // "C"
你以为这就完了?太年轻了兄弟
TCHAR
怎么办呢
难道你想说你看了眼TCAHR
的定义:typedef char TCHAR
,然后发现很合理,很正常
随便用个cout
、 printf
都能正常输出中文
那你有没有想过TCHAR
的T 是什么意思,_T
有什么用,L
有什么用
T
可以理解为TEXT
,意为文本,也就是会根据UNICODE
宏定义自动处理宽窄字符
cpp
#ifdef UNICODE
typedef WCHAR TCHAR;
#else
typedef char TCHAR;
_T
也是如此
cpp
#define _T(x) __T(x)
#define _TEXT(x) __T(x)
#define __T(x) L ## x // if UNICODE
#define __T(x) x
那么L
前缀呢,就是把字面量标记为宽字符
だから,我们需要同时考虑宽窄字符(UNICODE
宏是否定义)
cpp
#include "tchar.h"
_tprintf(_T("%s\n"), szPath); // TCHAR
可以用_tprintf
宏来自动选择printf
和wprintf
// 宏真神奇
// 顺便说一下,Windows API也常会提供两个版本,以A结尾(ANSI,单字节字符)和以W结尾(宽字符),同时还会提供一个宏(不带后缀)来自动选择
啊,我不得不吐槽一下,原生C++的编码太离谱了,不会真有人用得来吧
看看远方的Qt
吧,家人们,QString
直接就是Unicode
编码,舒服
你以为这就完了?仍旧年轻了兄弟
如果你用的是Windows Clion + MSVC
,此时你只要:
cpp
cout << "测试" << endl;
// 娴嬭瘯
就会得到这一坨,还有一个Waring:
cpp
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
,如果没有BOM
,MSVC
编译器就不会认为这是Unicode
编码,导致编译后打印乱码
Solution:
- 在IDE的文件编码设置中改为
UTF-8 with BOM
- 或在
cmake
中强制采用UTF
-8编译:add_compile_options(/utf-8)
还有个坑:
Clion
正常运行没问题,但是调试模式 还是会乱码,寄
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 支持
- 取消此电脑链接
- 移动(剪切)OneDrive文件夹
- 重新初始化OneDrive,并选择OneDrive文件夹位置(移动后)
好的,那么OneDrive中的Music文件夹会怎么样,能保持正宫位置吗
请看一段广告,我们稍后回来------
好的,没有赞助商,揭晓答案:OneDrive\Music
守住了宝座,非常正常,甚至注册表也同步更新了
为什么呢,究竟是被包装在OneDrive中的原因,还是OneDrive取消链接,从网盘退化为普通文件夹的原因!
为了验证,我们先取消OneDrive链接,使其退化为普通文件夹,然后剪切Music
文件夹
发现:还是没有卵用,Music
文件夹依旧退化
寄,OneDrive文件夹真神奇
Rebuild
如果我们把OneDrive\Music
删除会怎么样呢?
此时在user
文件夹下生成了一个新的Music文件夹
那么假如我们把这个C:\Users\{name}\Music
继续删除呢
emmm,那就没了
此时,SHGetKnownFolderPath
的返回Error
(hr != S_OK
),但是注册表中记录的还是默认位置 //看来会判断是否存在
如何找回呢?
OneDrive备份
如果我们直接打开OneDrive同步与备份中的Music开关
那么OneDrive中会奇迹般地出现"音乐"文件夹,同时注册表也更新了
API
其实我们也可以通过SHGetKnownFolderPath
手动新建系统文件夹
只要将dwFlags
设置为该值:
cpp
KF_FLAG_CREATE
值: 0x00008000
指定强制创建指定文件夹(如果该文件夹尚不存在)。 应用为该文件夹预定义的安全预配。 如果文件夹不存在且无法创建,则该函数将返回失败代码,并且不会返回任何路径
cpp
SHGetKnownFolderPath(FOLDERID_Music, KF_FLAG_CREATE, nullptr, &path);
这样,Music
文件夹就会在默认位置重新被创建了(Brand-New)!
// 如果想要获取某文件夹的默认存储位置(移动前的)可以这样:
cpp
SHGetKnownFolderPath(FOLDERID_Music, KF_FLAG_DONT_VERIFY | KF_FLAG_DEFAULT_PATH, nullptr, &path);
cpp
KF_FLAG_DEFAULT_PATH
值: 0x00000400
指定检索已知文件夹的默认路径。 如果未设置此标志,则该函数将检索文件夹的当前路径(可能重定向)。 除非设置了 KF_FLAG_DONT_VERIFY ,否则此标志的执行包括验证文件夹是否存在。
静观其变
既然我们可以通过API去创建个人文件夹,那么同理,其他软件如果需要往这些文件夹写入数据但是发现Not Found,可能大概应该会重新帮我们创建
所以,放几天大概就好了吧,啊哈哈
// 例如:打开网易云音乐就会自动创建
手动新建
或者,我们可以观察一下注册表中对应文件夹的位置,然后手动新建一个一毛一样的文件夹,然后重启资源管理器,大概就会被认为是正宫了
Just没有图标,那么文件夹图标是哪来的呢?
其实是文件夹下的一个隐藏文件(且被系统保护):desktop.ini
对于Music
文件夹,desktop.ini
内容如下:
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的资源(字符串)
我们可以验证一下:
cpp
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
的内容发生了变化
只剩下更改文件夹名称的这一行了(但其实真是文件名也变成了"音乐")
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
Qt获取windows文档、下载、图片等目录路径_qt获取download目录-CSDN博客
c++ - 获取我的文档的路径 - SegmentFault 思否
更改 OneDrive 文件夹的位置 - Microsoft 支持
移动Onedrive文件夹至D盘目录 - 知乎 (zhihu.com)
「带 BOM 的 UTF-8」和「无 BOM 的 UTF-8」有什么区别?网页代码一般使用哪个? - 知乎 (zhihu.com)
解决 C++ printf 汉字问号。含 _tprintf(), printf(), wprintf() 详解_c++汉字变成问号-CSDN博客
[C++] cout、wcout无法正常输出中文字符问题的深入调查(1):各种编译器测试 - zyl910 - 博客园 (cnblogs.com)