news 2026/10/29 4:51:38

Delphi 11 重编 DevExpress VCL 20.2.6:安装、注册与部署验收

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Delphi 11 重编 DevExpress VCL 20.2.6:安装、注册与部署验收

简介:这是一份面向Delphi 11开发者的DevExpress VCL 20.2.6控件安装包,主要解决旧版控件包升级后难以编译、网络资源版本混杂且可靠性不足的问题,适合正在进行开发环境迁移、需要恢复VCL界面组件库的中高级Delphi开发者使用。压缩包采用7z格式,整体约473.21MB,上游未提供文件总数与类型明细,但资源标题明确标注为非试用版,并说明已在Delphi 11中亲测通过;标签涉及完整源码与Delphi 11适配,配合DxAutoInstaller.exe安装入口可减少手工配置环节。已有516人学习或下载,说明该版本在Delphi升级场景中具备一定参考价值。对于希望缩短控件重装、编译排错与环境恢复时间的读者,这份安装包提供了较完整的DevExpress VCL组件基础,既可用于搭建新项目界面,也可用于旧系统维护时的组件补全与兼容性验证。

1. 拿到 DevExpress VCL 20.2.6 安装包,先想清楚它绑在哪一代编译器上

一个 DevExpress VCL 20.2.6 的控件安装包落到手里,多数人第一反应是双击 Setup、打开 Delphi 11、等组件面板自己长出 Grid 和 Bar。真拆开看会发现这套包是按编译器版本切开的:20.2.6 出厂时对的是 Delphi 10.4 及更早,Delphi 11 的 22.0 工具链并不在它的官方支持列表里。所以"能不能用"这个问题,答案不在安装器的欢迎页上,而在三件事:包工程能不能被 22.0 的 dcc32/msbuild 重新编出来、BPL 命名里的版本后缀与搜索路径有没有对齐、设计期包注册得干不干净。这篇面向手上已经有一个 20.2.6 安装包、机器上装着 Delphi 11、准备把它接进真实项目的人:从解包目录开始,到编译、注册、部署、验收,把每一步的命令和参数写死,也把会翻车的地方提前标出来。

2. 解包先看目录:控件安装包里到底有哪几类文件

2.1 Sources、Packages、Bin 三个目录各自管什么

安装包解开以后,一级目录看着杂,其实职责很清楚。分不清这三个目录,后面所有报错都会变成瞎猜。

Sources 是源码单元目录,按功能拆成 cxLibrary、dxCore、cxGrid、dxBar 这样一摞文件夹,每个文件夹里是 .pas 和 .inc。它是你在 Delphi 11 下重编的唯一依据:预编译产物跟编译器版本强绑定,跨版本一律不能用,只有源码能重新出 dcu。

Packages 是按 IDE 版本分的包工程目录,子目录名通常是 Delphi10_4 这类。每个子目录里放着 .dproj 和 .dpk,运行时包和设计期包成对出现,设计期包一般以 dcl 开头。这里有个关键点:20.2.6 的 Packages 下面大概率没有 Delphi11 这一层,你面对的是 10.4 的工程。常见做法是把最接近的那层整体复制一份、改名为 Delphi11,再用 IDE 打开让它自动升级工程格式,然后重编。

Bin 目录放的是厂商预编译好的 bpl 和 dcu,按 Win32/Win64 分。这部分在你重编之后就不能再用了,留着只会污染搜索路径,建议重编前先改名备份。

目录内容你在 Delphi 11 下要不要动
Sources全部 .pas/.inc 源码必须用,重编的唯一输入
Packages按 IDE 版本分的 .dproj/.dpk复制一份改名,作为编译入口
Bin预编译 bpl/dcu不要用,改名或移出搜索路径
Library安装后复制出来的公共库装完才有,检查路径是否指向它

注意:把 Bin 里的 dcu 和 Sources 重编出来的 dcu 混在同一个搜索路径里,是后面 F2051 报错最常见的来源,先隔离再说。

2.2 用一段 PowerShell 把包工程和依赖关系扫出来

包工程的编译顺序不能靠记忆排。DevExpress 的包之间是单向依赖,底层包编不出来,上层全废。与其一个个试,不如让脚本把依赖关系打印出来。

# 扫描 Packages 目录下所有包工程,打印它 requires 的其他包 # 输出里 Depends 为空的,就是最底层、必须先编的包 $pkgRoot = "D:\DX\DevExpress VCL 20.2.6\Packages\Delphi11" Get-ChildItem $pkgRoot -Recurse -Filter *.dproj | ForEach-Object { [xml]$xml = Get-Content -LiteralPath $_.FullName -Encoding UTF8 # dproj 带 MSBuild 命名空间,用 local-name() 绕开不同版本的前缀差异 $refs = $xml.SelectNodes("//*[local-name()='DCCReference']") | ForEach-Object { $_.GetAttribute('Include') } | Where-Object { $_ -notmatch '\.pas$' } # 单元引用不算包依赖 [pscustomobject]@{ Package = $_.BaseName Depends = ($refs | ForEach-Object { [IO.Path]::GetFileNameWithoutExtension($_) }) -join ' | ' } } | Sort-Object Package | Format-Table -Wrap -AutoSize

逻辑说明:dproj 本质是 MSBuild 的 XML,包之间的依赖写在 DCCReference 节点里,值可能是 .dcp 也可能是 .pas。过滤掉 .pas 之后剩下的就是真正的包依赖,谁的 Depends 为空谁就是第 0 层。

参数说明:$pkgRoot换成你自己的解包路径,路径带空格时 PowerShell 里用引号包住即可;-Recurse会连子目录一起扫,第一次跑可能要几秒;如果你的包工程是 ANSI 编码,把-Encoding UTF8去掉,否则中文注释多的工程可能读成乱码导致 XML 解析失败。

跑完你会看到一张表,大致能分成这么几层:

层级典型包说明
第 0 层ExpressLibrary 对应包几乎所有包都 requires 它,先编
第 1 层ExpressCore、GDI+ 相关包只依赖第 0 层
第 2 层DataController、Editors依赖前两层,控件的基础设施
第 3 层Grid、Bars、Scheduler、Layout具体控件,数量最多
第 4 层打印、导出、报表类包依赖第 3 层

2.3 授权状态:别用"非试用版"四个字安慰自己

标题里那句"非试用版",落到工程上其实是一个可验证的客观状态,不是一个形容词。判断标准只有两条:设计期把控件拖到窗体上不弹提示,以及编译出的 exe 在一台没装过这套控件的机器上运行不弹提示。两条都过,才算授权链路是通的。

安装器做完之后会把授权信息写进当前用户的配置区,IDE 启动加载设计期包时读的就是这份。想确认它到底写没写进去,别去猜键名,直接递归看结构:

rem 递归列出当前用户下与产品相关的配置键,只读不改 reg query "HKCU\Software" /s /f "DevExpress" /k 2>nul | more

逻辑说明:/s递归、/f过滤、/k只列键名,最后用 more 分页,避免刷屏。这条命令只读,不会改动任何东西。看到结构以后你就知道安装器把这套东西放在哪了,出问题时也知道去哪比对。

参数说明:不同版本的键名和层级不完全一样,所以这里用通配式搜索而不是写死路径;2>nul是为了吞掉无权限分支的报错输出。

必须说清楚的一点:授权是买来的,任何绕过手段都别碰。真实项目交付时,法务风险比编译错误难处理一百倍,一个 E2051 你能查一晚上,一份授权纠纷能拖半年。真要长期用,就按项目预算走正规采购,试用期评估就用试用版把技术路线跑通,别在授权上省。

3. 用 Delphi 11 的 dcc32 重编运行时包与设计期包

3.1 先 Runtime 后 Design,顺序反了必炸

一个控件库的包分两类:运行时包(runtime package)和设计期包(design-time package)。运行时包提供控件的实际功能,最终要跟 exe 一起部署;设计期包只负责把控件注册到 IDE 的组件面板上,装完不随程序发布。

依赖方向是单向的:设计期包 requires 运行时包,反过来不成立。所以编译顺序必须是先把所有 runtime 包装完、注册到系统里,再编 dcl 包。反过来做,IDE 在编译 dcl 包时会去找 runtime 包的 dcp,找不到就直接报"找不到必需的包"。

实际操作里还有个细节容易被忽略:设计期包编译时需要能找到 runtime 包的 .dcp 文件,而 .dcp 的输出位置由包工程自己的 DCC_DcpOutput 属性决定。如果你只改了一部分工程的输出路径,就会出现一半设计期包编得过、一半编不过的玄学现象。我一般的做法是统一用 msbuild 的/p:DCC_DcpOutput覆盖,让所有包输出到同一个目录,再在 IDE 里把这个目录加进搜索路径。

3.2 用 rsvars.bat 加 msbuild 编一个包的最小命令

命令行编译比在 IDE 里点 Build 可靠得多,因为 IDE 会带着一堆当前会话的状态,同样的工程换台机器结果可能不一样。固定用 rsvars.bat 初始化环境,是把变量收干净的最简单办法。

@echo off rem 锁定 Delphi 11 的 22.0 工具链,避免 PATH 里混进别的版本 set "BDS=C:\Program Files (x86)\Embarcadero\Studio\22.0" set "DXPKG=D:\DX\DevExpress VCL 20.2.6\Packages\Delphi11" set "DXOUT=D:\DX\build\dcp" call "%BDS%\bin\rsvars.bat" rem 先编第 0 层,这一层过不了,后面全白搭 "%BDS%\bin\msbuild.exe" "%DXPKG%\ExpressLibrary.dproj" ^ /t:Build /p:Config=Release /p:Platform=Win32 ^ /p:DCC_DcpOutput="%DXOUT%" /v:minimal /nologo if errorlevel 1 ( echo [FAIL] 第 0 层编译失败,先解决这里再往下 exit /b 1 ) echo [OK] 第 0 层完成

逻辑说明:rsvars.bat会设置 BDS、FrameworkDir、PATH 等一批环境变量,之后调用同目录下的 msbuild 就能保证用的是 22.0 的编译器而不是系统里别的版本。/t:Build是增量编译,比 Rebuild 快,也不会清掉已有的 dcu。

参数说明:/p:Config=Release指定配置,调试期可以换 Debug,但交付前一定用 Release 过一遍;/p:Platform=Win32只编 32 位,需要 64 位就再来一轮换 Win64,两套产物要分目录存放;/p:DCC_DcpOutput把 dcp 统一到一个目录,方便后面加搜索路径;/v:minimal只打摘要,出错了再单独编一次看详细日志;/nologo去掉版权头,日志干净点。

验完第 0 层,再按依赖顺序往下推。手写一长串命令太累,可以按名字匹配批量跑:

rem 按依赖层级从低到高,逐层推进;每层跑完检查 errorlevel for %%P in ("%DXPKG%\*Core*.dproj" "%DXPKG%\*GDIPlus*.dproj") do ( echo === %%~nxP "%BDS%\bin\msbuild.exe" "%%~fP" /t:Build /p:Config=Release ^ /p:Platform=Win32 /p:DCC_DcpOutput="%DXOUT%" /v:quiet /nologo if errorlevel 1 exit /b 1 )

逻辑说明:for %%P in (...)里的模式串就是你的层级清单,按层往下加,别图省事直接用*.dproj一把梭——文件名排序和依赖顺序没有任何关系,一把梭的结果是前几轮大量失败,你会误以为是环境问题。

参数说明:%%~nxP取文件名带扩展名用于打印,%%~fP取完整路径传给 msbuild;/v:quiet比 minimal 更安静,适合批量跑。

3.3 把设计期包装进 IDE 的两条路

所有 runtime 包编完,就该处理 dcl 包了。第一条路是 IDE 里操作:Component → Install Packages → Add,选编译输出目录里 dcl 开头的 bpl。装完组件面板上会出现对应的页签。这条路直观,缺点是每次重装都要点一遍,而且一旦装错版本,卸载要靠记忆。

第二条路是改注册表的 Known Packages。这个键在 HKCU 下按 IDE 版本分,Delphi 11 对应 22.0。手改之前先备份,这是后悔药:

rem 先备份,再考虑改;出问题直接用备份回滚 reg export "HKCU\Software\Embarcadero\BDS\22.0\Known Packages" ^ "%USERPROFILE%\Desktop\known_packages_backup.reg" /y

逻辑说明:reg export 把整个键导成 .reg 文件,回滚时双击导入即可。养成改注册表前先导出的习惯,比事后翻文档快得多。

参数说明:路径里的 22.0 是 Delphi 11 的 IDE 版本号,别写错;如果你的 IDE 是装在别的用户下,注意要在同一个账户下执行,因为 HKCU 是按用户隔离的。

装完打开 IDE,看组件面板有没有 DevExpress 页签。没有的话别急着怀疑编译,先去 Install Packages 列表里看 dcl 包在不在、路径对不对——多数"装完没反应"是路径指向了旧的输出目录。

4. 搜索路径、环境变量与多 Delphi 版本共存

4.1 编译期找 dcu、运行期找 bpl,这是两套东西

这是新手最容易混的一点。编译期 dcc32 需要找到 .dcu 才能把单元链起来,靠的是 unit 搜索路径;运行期 exe 需要找到 .bpl 才能加载包,靠的是 Windows 的 DLL 搜索顺序。两套路径互不相干,只配好一套的结果就是"编译通过、一运行就炸"。

编译期的路径有三个地方可以配:IDE 全局的 Library 路径、项目自己的搜索路径、命令行-U参数。三者优先级是项目 > 全局。我一般不用 IDE 全局配置,因为那东西一旦配错,机器上所有项目都受影响,排查起来等于黑匣子。用命令行参数或者项目级配置,出问题范围可控。

rem 直接命令行编译一个用到控件库的工程 rem -U 编译期找 dcu,-I 找 inc,-E 输出 exe,-N0 输出 dcu 中间产物 dcc32 -B ^ -U"D:\DX\DevExpress VCL 20.2.6\Library\Delphi11\Win32\Release" ^ -I"D:\DX\DevExpress VCL 20.2.6\Sources" ^ -E"D:\build\bin" ^ -N0"D:\build\dcu" ^ DemoApp.dpr

逻辑说明:-U指向重编后的 dcu 目录,必须是你自己编出来的那份,不能是安装包 Bin 里的;-I指向源码里的 inc 文件,很多控件单元会 include 公共定义;-E决定 exe 落在哪;-N0把中间 dcu 单独放,避免和库的 dcu 混在一个目录里互相覆盖。

参数说明:-B是全量重建,第一次或改过搜索路径后用,日常增量去掉它;路径含空格时整个参数要用引号包住,写法是-U"带 空格 的路径",引号位置错了 dcc32 会把路径截断,报错还会误导你以为文件不存在。

运行期就简单了:把用到的 bpl 放到 exe 同目录,或者放到 PATH 里的某个目录。Windows 加载 DLL 的顺序里 exe 所在目录优先级很高,所以同目录是最稳的做法。把 bpl 塞进 System32 是典型的踩坑操作,短期能跑,换台机器或者系统更新就出问题,而且卸载时根本清不干净。

4.2 用批处理固化一套构建环境

手工敲命令做一次可以,天天做迟早出错。把环境写进一个 build.bat,所有人用同一份,问题才可复现。

@echo off setlocal rem 所有可变项集中在这里,换机器只改这一段 set "BDS=C:\Program Files (x86)\Embarcadero\Studio\22.0" set "DXROOT=D:\DX\DevExpress VCL 20.2.6" set "DXLIB=%DXROOT%\Library\Delphi11\Win32\Release" set "OUT=%~dp0build" if not exist "%OUT%" mkdir "%OUT%" call "%BDS%\bin\rsvars.bat" rem 先清掉上次的中间产物,避免脏 dcu 混进来 if exist "%OUT%\dcu" rd /s /q "%OUT%\dcu" dcc32 -B -U"%DXLIB%" -I"%DXROOT%\Sources" ^ -E"%OUT%\bin" -N0"%OUT%\dcu" "%~dp0DemoApp.dpr" if errorlevel 1 ( echo [FAIL] 编译失败,检查 DXLIB 是否指向本次重编的产物 exit /b 1 ) echo [OK] 输出在 %OUT%\bin

逻辑说明:setlocal保证变量不会污染调用者的环境;%~dp0取脚本所在目录,这样脚本放哪都能跑;先删 dcu 目录是为了避免上次的中间产物被误用,这类脏数据导致的报错极难定位。

参数说明:rd /s /q会静默删目录,路径一定要拼对,别把变量写成空值——if exist那行就是保险。exit /b 1把错误码传出去,方便接到更上层的流水线里。

4.3 多版本共存:靠目录隔离,不靠环境变量

机器上同时有 Delphi 10.4 和 11 很常见。两套 IDE 的 dcu 格式不兼容,混用的结果就是编译期报"单元是用不同版本编译的"。解决办法只有一个:物理隔离。每套 Delphi 对应一份自己的 dcu 输出目录,命名里带版本号,比如Library\Delphi10_4\...和Library\Delphi11\...,绝不共用。

IDE 全局 Library 路径里也别同时挂两个版本。如果确实要挂,确保项目级路径覆盖它。曾经见过一个案例:全局路径指向 10.4 的库,项目级什么都没写,编译能过,但只要换个人打开工程就报错——因为那个人的全局路径配置不一样。这类问题的排查成本极高,预防成本却几乎为零。

5. 五个真实翻车现场:从找不到单元到设计期包加载失败

5.1 编译阶段的三类报错

现象一:F2051 Unit cxXXX was compiled with a different version of cxLibrary。原因:搜索路径里同时存在两份不同编译器编出来的 dcu,或者一份来自安装包 Bin、一份来自你自己重编。dcc32 找到哪个用哪个,找到旧的就报版本不一致。 解决:把 Bin 目录改名移出搜索路径,把自己的输出目录清空重编,然后用-U只指向唯一一个目录。确认方法是在编译日志里加-B,看它到底去哪个目录找的单元。

现象二:E2003 Identifier expected,报在某个源码文件的奇怪位置。原因:文件编码问题。老版本的源码里有大量 ANSI 编码的 .pas,Delphi 11 的编译器对编码更敏感,某些字符会被解析错位,报错行号还指在看起来完全正常的代码上。 解决:先确认报错文件是不是从安装包里直接拿的,如果是,检查有没有被别的工具转存过。常见的做法是保持源码原样不动,只在工程层面加路径,别用编辑器批量"另存为 UTF-8"——那会把整个文件的字节序改掉。

现象三:E2202 Required package 'xxx' not found。原因:设计期包找不到对应的运行时包 dcp,或者 dcp 的输出路径和搜索路径对不上。 解决:先用 2.2 的脚本确认这个运行时包确实编出来了,再看它的 DCC_DcpOutput 指向哪。批量编译时如果只给部分工程加了输出参数,就会出现这种一半成功一半失败的局面。

5.2 设计期与部署阶段的翻车

现象四:装设计期包时提示 Cannot load package,说某个单元同时存在于两个包里。原因:机器上装过这套控件的另一个大版本,旧包的注册项还在 Known Packages 里,新包加载时发现单元冲突。 解决:先把旧包从 Install Packages 里全部移除,再检查 Known Packages 键里有没有残留项,清干净之后再装新的。这就是 3.3 里那份备份的用处——改坏了能一键回滚,不用重装 IDE。

现象五:开发机上一切正常,交付到客户机上运行就提示找不到 bpl。原因:开发机上 bpl 在 PATH 里,客户机上没有;或者只拷了 exe 没拷 bpl。 解决:交付包必须包含所有实际用到的 bpl,并且放在 exe 同目录。判断"实际用到哪些"不要靠猜,用依赖查看工具扫 exe,或者干脆在干净虚拟机上跑一遍完整流程。这条血泪经验值得写成发布检查项,每次发版都过一遍。

6. 验收:怎么证明这套控件在 Delphi 11 里是真跑起来了

标题里那句"亲测通过",落到可复现的标准上就是下面四条,缺一条都不算装好:

验收项操作通过标准
设计期注册打开 IDE 看组件面板出现对应页签,控件能拖到窗体上且不弹提示
编译期链接新建 VCL 应用,uses 一个控件单元并编译无 F2051、无 E2202,编译通过
运行期加载把 exe 拷到没装过这套控件的机器上运行正常启动,无"找不到模块"弹窗
授权状态在干净机器上打开窗体不出现试用提示

第三条最容易被跳过,也最容易出问题。开发机上什么都有,跑通不代表能交付。准备一台干净虚拟机,或者至少用一个没配过 PATH 的账户跑一遍,比在开发机上反复确认一百次都管用。

把验收做成一条命令,比写一堆检查清单更实际。下面这个小程序只做一件事:静态引用几个关键控件单元,创建并释放一个控件实例。能正常退出,就说明整条包链是通的。

program DXSmoke; {$APPTYPE CONSOLE} uses System.SysUtils, cxLibrary, cxGrid, cxGridDBTableView; // 引用即触发运行时包加载 var Grid: TcxGrid; begin try Grid := TcxGrid.Create(nil); try Writeln('VCL 运行时包加载正常,TcxGrid 已创建并释放'); finally Grid.Free; end; except on E: Exception do begin // 把异常类型和消息都打出来,比只打一句 failed 有用得多 Writeln('FAIL: ', E.ClassName, ' -> ', E.Message); ExitCode := 1; end; end; end.

逻辑说明:uses 里出现控件单元,链接器就会在启动时加载对应的 bpl。如果 bpl 缺失或版本不对,异常会在单元初始化阶段抛出,被这里的 try 捕获,你拿到的是具体类名和消息,而不是一个笼统的启动失败。ExitCode 设为 1 是为了能接进脚本判断。

参数说明:编译时同样要带-U指向重编的 dcu 目录;运行前把需要的 bpl 放到 exe 同目录或者 PATH 里。这个程序不要放进正式项目,它只是个探针,用完就删。

我自己吃过最大的亏,是早期图省事直接在 IDE 里点 Build,觉得编译通过就等于装好了。结果交付前一夜发现客户机跑不起来,排查到凌晨才定位到一个 bpl 没进发布包。从那以后我的习惯是:任何控件库接入,先写一个这样的探针程序,开发机跑一遍、干净机器跑一遍,两条都绿了才动业务代码。这套流程多花二十分钟,省下的是整晚的返工。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 4:51:30

EtherCAT FMMU 地址映射原理与硬件配置详解

1. FMMU 不是缩写游戏,而是 EtherCAT 系统里真正管“地址搬运”的调度员你第一次在 EtherCAT 主站配置界面看到“FMMU”三个大写字母时,大概率会愣一下——它不像 PDO、SDO 那样在 CANopen 里就耳熟能详,也不像 DC(Distributed Cl…

作者头像 李华
网站建设 2026/10/29 4:51:29

Agent-Reach:用CLI+Python让AI Agent真正下地干活

1. 项目缘起:为什么我要折腾一个叫 Agent-Reach 的东西先说说这个项目是怎么来的。过去大半年,我一直在用各种 AI Agent 帮我处理日常的重复性工作,比如自动整理资料、批量处理表格、定时抓取一些公开数据做分析。用得多了就发现一个很尴尬的…

作者头像 李华
网站建设 2026/10/29 4:49:46

30天Caveman实验:回归原始生活方式改善代谢、睡眠与专注力

caveman三十天前,我在朋友圈看到朋友转的一张照片:凌晨两点,我对着亮得刺眼的屏幕,表情浮肿,桌上摆着三罐功能饮料和半袋薯片。下面有人评论:"你是当代穴居人吧。"这句随口调侃像根刺扎进了脑子里…

作者头像 李华
网站建设 2026/10/29 4:49:37

GTNH整合包汉化指南:从资源包结构到任务书替换的完整流程

1. 为什么一个整合包的汉化值得单独写一篇指南如果你玩过大型模组整合包,大概都有过这种体验:开局一片茫然,任务书里全是英文,物品名看得懂一半,提示信息靠猜,关键合成链因为一个词理解偏差直接卡两小时。G…

作者头像 李华
网站建设 2026/10/29 4:48:57

AI Skills工程化:GKE上生产级Skill七层部署与契约治理

1. “Skills”不是功能按钮,而是Agent时代的底层能力封装范式你点开一个AI工具,界面上写着“Add Skill”——这词看着亲切,像极了手机App里“添加插件”“启用扩展”。但如果你真把它当成普通功能开关来用,十有八九会在三天内陷入…

作者头像 李华
网站建设 2026/10/29 4:47:31

马尾辫好看不是玄学:从分区扎法到发圈选型的完整实操指南

扎马尾这件事,看似谁都会,但你仔细回想一下身边人的"马尾辫"——有的紧贴头皮像刚被揪起来的小兔子,有的绑了一下午就开始往下滑,有的后脑勺扁平得像个锅盖。但总有人随手一扎就好看,侧面轮廓圆润饱满&#…

作者头像 李华