Effect·팝업이 ClearType 을 끄고 있었다 — 글자가 흐린 이유

사용자 지적이 맞았다. Effect 가 걸린 요소는 하위 트리가 중간 서피스로 렌더링되면서
ClearType 이 꺼지고, 글자가 회색 안티에일리어싱으로 그려진다. 13px 한글이 특히 뭉개진다.

저장소는 이 사실을 이미 알고 있었다 — MessageDialogView.xaml:18 과 TagPickerDialogView.xaml:11 이
"AllowsTransparency 를 켜면 ClearType 이 꺼져 13px 한글이 뭉개진다"고 적어 두었다.
그런데 그 판단이 <b>창에만</b> 적용되고 Effect 와 Popup 에는 적용되지 않았다.
ClearTypeHint 는 저장소 전체에 0 곳이었다.

추론으로 끝내지 않았다. RenderTargetBitmap 으로는 이 질문에 답할 수 없다 —
그 자체가 알파를 가진 중간 서피스라서 Effect 가 있든 없든 양쪽 다 회색으로 나오고,
검사가 통과하면서 아무것도 증명하지 못한다. 그래서 진짜 화면을 BitBlt 으로 떠서
글리프의 채널 어긋난 픽셀(ClearType 은 R≠G≠B, 회색 AA 는 R=G=B)을 셌다.

--cleartype 표본 11종 실측:
  A 아무것도 없음            색번짐 1115  ← 대조군. 이게 0 이면 판정 불가로 끝낸다
  B Effect                    색번짐    0  ← 지적한 그대로
  C1 힌트를 Effect 요소 자신에게  색번짐    0  ← 듣지 않는다
  C2 힌트를 안쪽 불투명 배경에    색번짐 1344  ← 여기가 맞는 자리
  D 레이어드 팝업              색번짐    0  ← 두 번째 원인
  E 팝업 + 힌트                색번짐 1115  ← AllowsTransparency 를 끌 필요가 없다
  I 힌트 바깥에 ScrollViewer   색번짐    0  ← ScrollViewer 도 중간 서피스를 연다
  J 힌트를 ScrollViewer 안쪽에  색번짐 1115
  F 앱 콤보 드롭다운(실제 템플릿) 색번짐    0 → 수리 후 1115

두 번 헛짚었고 두 번 다 측정이 정정했다.
CornerRadius 를 의심했으나 아니었고, ScrollViewer 가 범인이었다.
그리고 F 를 처음 통과시켰을 때는 선택된 콤보 항목의 파란 배경(B.Sel, 채널 차이 61)이
색번짐으로 잡힌 것이었다 — 색번짐 4359 인데 글자픽셀은 1075 였다.
그래서 배경이 유채색이면 판정을 거부하는 장치를 넣었다.

규칙: 힌트는 Effect·ScrollViewer·레이어드 팝업 <b>안쪽</b>의 불투명 배경 요소에 건다.

적용한 곳 — 글자가 많은 순:
  콤보 드롭다운(DesignerTheme) · 메뉴 하위팝업(배치·정렬 명령이 다 여기 있다) ·
  색 팝오버 · 컨트롤 추가 플라이아웃 · 줌 플라이아웃

남긴 곳: 플로팅 바(글자는 줌 퍼센트뿐), QueryEditorWindow 의 Card 스타일
(Effect 가 DataGrid·편집기의 중첩 ScrollViewer 를 감싸서 힌트를 넣을 자리가 여럿이다 —
그림자를 빼는 쪽이 맞아 보이고, 그건 디자인 결정이라 따로 다룬다).

게이트: --cleartype 11/11, 테스트 277/277, --edit-smoke 0실패, --modal-check 0실패,
--maxrect 0실패, --dialog-shots 57파일, --db-smoke 1271건 diff 0,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일(종이 렌더 불변).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-14 16:11:23 +09:00
co-authored by Claude Opus 5
parent b0d2e9b470
commit 280594362a
5 changed files with 481 additions and 5 deletions
+5
View File
@@ -287,6 +287,11 @@ public partial class App : Application
return Diagnostics.MaximizeCheck.Run(args.Length >= 2 ? args[1] : null);
}
if (args.Length >= 1 && args[0] == "--cleartype")
{
return Diagnostics.ClearTypeCheck.Run();
}
// 진단 모드에서는 모달을 띄우지 않는다 — 스크립트 옵션 오타 하나로 무인 실행이 여기서
// 영원히 멈춘다. WinExe 라 콘솔이 없어 stderr 는 호출자가 리다이렉트할 때만 보이므로
// 로그를 함께 남긴다(--db-render 가 .err.txt 로 남기는 것과 같은 이유).
@@ -0,0 +1,450 @@
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
namespace SheetMe.Designer.Diagnostics;
/// <summary>
/// ClearType 이 실제로 꺼지는지 <b>화면을 찍어서</b> 잰다 (--cleartype).
///
/// <b>왜 화면을 찍는가.</b> <see cref="RenderTargetBitmap"/> 으로는 이 질문에 답할 수 없다 —
/// 그 자체가 알파를 가진 중간 서피스라서 ClearType 이 무조건 꺼진다.
/// 오프스크린으로 재면 Effect 가 있든 없든 양쪽 다 회색으로 나오고,
/// 검사는 <b>통과하면서 아무것도 증명하지 못한다</b>.
/// 그래서 진짜 화면(데스크톱 DC)을 BitBlt 으로 떠서 픽셀을 본다.
///
/// <b>무엇을 세는가.</b> ClearType 은 서브픽셀 렌더링이라 글리프 경계에 <b>채널이 어긋난 픽셀</b>
/// (R≠G≠B, 이른바 색 번짐)을 남긴다. 회색 안티에일리어싱은 R=G=B 만 남긴다.
/// 그래서 "채널 차이가 있는 픽셀 수"가 곧 ClearType 이 켜졌는지의 지표다.
///
/// <b>스스로 속지 않기 위한 장치.</b> 대조군(A: 아무 효과 없는 글자)에서 색 번짐이 0 이면
/// 시스템 ClearType 이 꺼졌거나 캡처 경로가 틀린 것이므로 <b>판정 불가</b>로 끝낸다.
/// 그 확인 없이 B 가 0 인 것을 근거로 삼으면 "둘 다 0"을 결론으로 착각한다.
/// </summary>
internal static class ClearTypeCheck
{
#region Member Fields
/// <summary>이 정도 채널 차이면 회색 AA 가 아니다 — 회색은 원리상 차이가 정확히 0 이다</summary>
private const int ChannelSpread = 6;
/// <summary>글자가 그려진 픽셀로 볼 밝기 상한(흰 배경 기준)</summary>
private const int InkLuma = 200;
private static readonly List<string> Lines = new();
private static int failures;
#endregion
#region Methods
public static int Run()
{
// F 표본이 앱의 실제 콤보 템플릿을 타야 하므로 테마를 실제로 적용한다
Services.ThemeManager.Apply(true);
var window = BuildProbe(out var samples, out var popups);
window.Show();
// 창이 보이는 것과 합성기가 그려 낸 것은 다르다 — Loaded 직후 찍으면 빈 화면을 잰다.
// (--modal-check 에서 BeginInvoke 가 Loaded 보다 먼저 돌아 0 을 세던 것과 같은 함정)
var timer = new System.Windows.Threading.DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(600),
};
timer.Tick += (_, _) =>
{
timer.Stop();
try
{
Judge(samples);
}
finally
{
foreach (var popup in popups)
{
popup.IsOpen = false;
}
window.Close();
Application.Current.Shutdown();
}
};
timer.Start();
var dispatcherFrame = new System.Windows.Threading.DispatcherFrame();
Application.Current.Exit += (_, _) => dispatcherFrame.Continue = false;
System.Windows.Threading.Dispatcher.PushFrame(dispatcherFrame);
foreach (var line in Lines)
{
Console.Error.WriteLine(line);
}
Console.Error.WriteLine($"결과: 실패 {failures}건");
return failures == 0 ? 0 : 1;
}
/// <summary>대조군이 먼저 서야 나머지 숫자가 뜻을 갖는다</summary>
private static void Judge(List<(string Name, FrameworkElement Target, bool ExpectClearType)> samples)
{
var measured = samples
.Select(s => (s.Name, s.ExpectClearType, Result: Measure(s.Target)))
.ToList();
foreach (var (name, _, result) in measured)
{
// 잰 사각형을 함께 찍는다 — 두 표본이 같은 자리를 재고 있으면 숫자가 우연히 같아지는데,
// 좌표를 안 찍으면 그것을 "결과가 같다"로 읽는다(실제로 한 번 그렇게 읽었다).
Lines.Add($" {name,-40} 색번짐 {result.Fringed,6} / 글자픽셀 {result.Ink,6} @ {result.Rect}");
}
var control = measured[0];
if (control.Result.Fringed == 0)
{
Lines.Add("판정 불가 — 대조군에서 색 번짐이 0 이다.");
Lines.Add(" 시스템 ClearType 이 꺼져 있거나 캡처가 화면을 못 읽고 있다.");
Lines.Add(" 이 상태에서 나머지 0 은 근거가 되지 않는다.");
failures++;
return;
}
Lines.Add($"대조군 확인 — 색 번짐 {control.Result.Fringed}건. 이제 나머지 0 은 뜻이 있다.");
foreach (var (name, expect, result) in measured)
{
var on = result.Fringed > 0;
Check($"{name} → ClearType {(expect ? "" : "")}", on == expect,
$"실제 색번짐 {result.Fringed}건");
}
}
private static int Spread(int r, int g, int b)
=> Math.Max(r, Math.Max(g, b)) - Math.Min(r, Math.Min(g, b));
private static void Check(string label, bool ok, string detail = "")
{
Lines.Add($"{(ok ? "PASS" : "FAIL")} {label}{(detail.Length > 0 ? $" {detail}" : string.Empty)}");
if (!ok)
{
failures++;
}
}
#endregion
#region Probe
/// <summary>
/// 같은 글자를 다섯 조건으로 놓는다. 배경은 전부 <b>불투명 흰색</b>이어야 한다 —
/// ClearType 은 뒤에 무엇이 있는지 알아야 서브픽셀을 계산할 수 있어서,
/// 투명 배경에서는 효과와 무관하게 회색으로 떨어진다.
/// </summary>
private static Window BuildProbe(
out List<(string Name, FrameworkElement Target, bool ExpectClearType)> samples,
out List<Popup> popups)
{
var shadow = new DropShadowEffect
{
BlurRadius = 26, ShadowDepth = 5, Direction = 270,
Opacity = 0.14, Color = Colors.Black, RenderingBias = RenderingBias.Quality,
};
var plain = Text();
var effected = Text();
var hintOnEffected = Text();
var hintInside = Text();
var inPopup = Text();
var inPopupHinted = Text();
var effectedHost = Card(effected);
effectedHost.Effect = shadow;
// 변형 1 — 힌트를 Effect 가 걸린 요소 자신에게
var hostA = Card(hintOnEffected);
hostA.Effect = shadow.Clone();
RenderOptions.SetClearTypeHint(hostA, ClearTypeHint.Enabled);
// 변형 2 — 힌트를 중간 서피스 <b>안쪽</b>의 불투명 배경 요소에.
// 문서가 "불투명 배경이 있는 지점"이라고만 말해서 어느 쪽인지 재 봐야 안다.
var inner = new Border { Background = Brushes.White, Child = hintInside };
RenderOptions.SetClearTypeHint(inner, ClearTypeHint.Enabled);
var hostB = Card(inner);
hostB.Effect = shadow.Clone();
// F — 합성 표본이 아니라 <b>앱의 실제 콤보 템플릿</b>. ComboBox 스타일이 암시라 그대로 태워진다.
// 이 표본이 있어야 "플랫폼이 그렇다"에서 "이 앱이 그렇다"로 넘어갈 수 있다.
// 드롭다운은 AllowsTransparency 팝업 + FloatShadow Effect 가 겹쳐 걸린 자리다(DesignerTheme.xaml:548,553).
var themedItem = Text();
var combo = new ComboBox { Width = 300, HorizontalAlignment = HorizontalAlignment.Left };
combo.Items.Add(themedItem);
// 선택하지 않는다 — 선택 강조가 배경을 유채색으로 만들면 위 판정 장치가 이 표본을 거부한다
combo.SelectedIndex = -1;
var anchor = new Border { Width = 300, Height = 20, Background = Brushes.White };
var anchorHinted = new Border { Width = 300, Height = 20, Background = Brushes.White };
var anchorStacked = new Border { Width = 300, Height = 20, Background = Brushes.White };
var anchorSplit = new Border { Width = 300, Height = 20, Background = Brushes.White };
var anchorScrolled = new Border { Width = 300, Height = 20, Background = Brushes.White };
var anchorInnermost = new Border { Width = 300, Height = 20, Background = Brushes.White };
var popupPlain = LayeredPopup(inPopup, anchor);
var popupHinted = LayeredPopup(inPopupHinted, anchorHinted);
RenderOptions.SetClearTypeHint((FrameworkElement)popupHinted.Child, ClearTypeHint.Enabled);
// G — 앱의 현재 구조: 레이어드 팝업 <b>안에</b> Effect 가 걸린 Border, 그 안에 힌트.
// 중간 서피스가 두 겹이면 힌트가 듣는지 재 본다.
var stackedText = Text();
var stackedInner = new Border { Background = Brushes.White, Child = stackedText };
RenderOptions.SetClearTypeHint(stackedInner, ClearTypeHint.Enabled);
var stackedOuter = new Border { Background = Brushes.White, Child = stackedInner, Effect = shadow.Clone() };
var popupStacked = LayeredPopup(stackedOuter, anchorStacked, wrap: false);
// H — 후보 수리: 그림자를 <b>글자가 없는 형제</b>로 떼어낸다.
// 그러면 Effect 의 중간 서피스에 글자가 안 들어가고, 남는 것은 레이어드 창 한 겹뿐이라
// E 와 같은 조건이 된다.
var splitText = Text();
var shadowOnly = new Border { Background = Brushes.White, Effect = shadow.Clone() };
var splitContent = new Border { Background = Brushes.White, Child = splitText };
RenderOptions.SetClearTypeHint(splitContent, ClearTypeHint.Enabled);
var splitHost = new Grid();
splitHost.Children.Add(shadowOnly);
splitHost.Children.Add(splitContent);
var popupSplit = LayeredPopup(splitHost, anchorSplit, wrap: false);
// I — G 와 똑같지만 힌트와 글자 사이에 ScrollViewer 를 끼운다.
// 앱 콤보 드롭다운이 바로 이 모양인데 G 는 되고 F 는 안 되니, 남은 차이가 이것뿐이다.
var scrolledText = Text();
var scrolledInner = new Grid { Background = Brushes.White };
RenderOptions.SetClearTypeHint(scrolledInner, ClearTypeHint.Enabled);
scrolledInner.Children.Add(new ScrollViewer { Content = scrolledText });
var scrolledOuter = new Border
{
Background = Brushes.White, Child = scrolledInner, Effect = shadow.Clone(),
};
var popupScrolled = LayeredPopup(scrolledOuter, anchorScrolled, wrap: false);
// J — 힌트를 ScrollViewer <b>안쪽</b>(가장 안쪽 불투명 배경)에 건다.
var innermostText = Text();
var innermost = new Border { Background = Brushes.White, Child = innermostText };
RenderOptions.SetClearTypeHint(innermost, ClearTypeHint.Enabled);
var innerOuter = new Border
{
Background = Brushes.White,
Child = new ScrollViewer { Content = innermost },
Effect = shadow.Clone(),
};
var popupInnermost = LayeredPopup(innerOuter, anchorInnermost, wrap: false);
var stack = new StackPanel { Margin = new Thickness(20) };
stack.Children.Add(Card(plain));
stack.Children.Add(effectedHost);
stack.Children.Add(hostA);
stack.Children.Add(hostB);
stack.Children.Add(anchor);
// 팝업이 아래로 펼쳐질 자리 — 앵커끼리 겹치면 두 팝업이 같은 픽셀을 재게 된다
stack.Children.Add(new Border { Height = 46 });
stack.Children.Add(anchorHinted);
stack.Children.Add(new Border { Height = 46 });
stack.Children.Add(combo);
stack.Children.Add(new Border { Height = 46 });
stack.Children.Add(anchorStacked);
stack.Children.Add(new Border { Height = 46 });
stack.Children.Add(anchorSplit);
stack.Children.Add(new Border { Height = 46 });
stack.Children.Add(anchorScrolled);
stack.Children.Add(new Border { Height = 46 });
stack.Children.Add(anchorInnermost);
stack.Children.Add(new Border { Height = 46 });
var window = new Window
{
Title = "ClearType 검사",
Width = 480, Height = 1040,
Left = 60, Top = 60,
WindowStartupLocation = WindowStartupLocation.Manual,
Background = Brushes.White,
Content = stack,
Topmost = true,
};
window.ContentRendered += (_, _) =>
{
popupPlain.IsOpen = true;
popupHinted.IsOpen = true;
popupStacked.IsOpen = true;
popupSplit.IsOpen = true;
popupScrolled.IsOpen = true;
popupInnermost.IsOpen = true;
combo.IsDropDownOpen = true;
};
// 기대값은 <b>예측이 아니라 측정 결과</b>다. C1 과 E 는 내가 예상한 것과 반대로 나왔고
// 그 사실이 곧 수리 방법을 정한다 —
// 힌트는 Effect 가 걸린 요소 자신에게 걸면 듣지 않고(C1), 그 <b>안쪽</b> 불투명 배경에 걸면 듣는다(C2).
// 레이어드 팝업도 자식 불투명 배경에 걸면 되살아난다(E) — AllowsTransparency 를 끌 필요가 없다.
samples = new()
{
("A 아무것도 없음 (대조군)", plain, true),
("B Effect (그림자)", effected, false),
("C1 Effect + 힌트를 그 요소에", hintOnEffected, false),
("C2 Effect + 힌트를 안쪽 배경에", hintInside, true),
("D Popup AllowsTransparency", inPopup, false),
("E Popup + 힌트를 자식 배경에", inPopupHinted, true),
("F 앱 콤보 드롭다운 (실제 템플릿)", themedItem, true),
("G 팝업+Effect 두 겹 + 힌트", stackedText, true),
("H 팝업, 그림자를 형제로 분리 + 힌트", splitText, true),
("I 힌트 바깥에 ScrollViewer", scrolledText, false),
("J 힌트를 ScrollViewer 안쪽에", innermostText, true),
};
popups = new() { popupPlain, popupHinted, popupStacked, popupSplit, popupScrolled, popupInnermost };
return window;
}
/// <summary>글리프가 많고 획이 가는 글자 — 색 번짐이 가장 잘 드러난다</summary>
private static TextBlock Text() => new()
{
Text = "가나다라마 환자명 일자 ABCDEFG 0123456789",
FontFamily = new FontFamily("Malgun Gothic"),
FontSize = 13,
Foreground = Brushes.Black,
Margin = new Thickness(8, 5, 8, 5),
};
private static Border Card(UIElement child) => new()
{
Background = Brushes.White,
Margin = new Thickness(0, 0, 0, 18),
Child = child,
};
private static Popup LayeredPopup(UIElement child, UIElement target, bool wrap = true) => new()
{
PlacementTarget = target,
Placement = PlacementMode.Bottom,
AllowsTransparency = true,
StaysOpen = true,
Child = wrap ? new Border { Background = Brushes.White, Child = child } : child,
};
#endregion
#region Screen capture
/// <summary>
/// 이 요소가 실제로 그려진 화면 조각을 떠서, 채널이 어긋난 픽셀과 글자 픽셀을 센다.
/// </summary>
private static (int Fringed, int Ink, string Rect) Measure(FrameworkElement element)
{
if (!element.IsVisible || element.ActualWidth < 1 || element.ActualHeight < 1)
{
return (0, 0, "보이지 않음");
}
// PointToScreen 은 물리 화면 픽셀을 준다 — 배율이 걸려 있어도 그대로 쓸 수 있다
var topLeft = element.PointToScreen(new Point(0, 0));
var bottomRight = element.PointToScreen(new Point(element.ActualWidth, element.ActualHeight));
var x = (int)Math.Floor(Math.Min(topLeft.X, bottomRight.X));
var y = (int)Math.Floor(Math.Min(topLeft.Y, bottomRight.Y));
var width = (int)Math.Ceiling(Math.Abs(bottomRight.X - topLeft.X));
var height = (int)Math.Ceiling(Math.Abs(bottomRight.Y - topLeft.Y));
if (width < 1 || height < 1)
{
return (0, 0, "크기 0");
}
var pixels = Grab(x, y, width, height);
if (pixels is null)
{
return (0, 0, "캡처 실패");
}
// 배경을 먼저 정한다 — 가장 많이 나온 색이 배경이다.
//
// <b>왜 필요한가.</b> "채널 차이 = 서브픽셀 AA"는 배경이 무채색일 때만 성립한다.
// 유채색 배경(예: 선택된 콤보 항목의 B.Sel #C2E2FF, 채널 차이 61)에서는
// 글자와 무관하게 거의 모든 픽셀이 색 번짐으로 잡혀서 <b>ClearType 이 꺼져 있어도 통과한다</b>.
// 실제로 그렇게 통과시켰다 — 색번짐 4359 인데 글자픽셀은 1075 였다.
var histogram = new Dictionary<int, int>();
for (var i = 0; i + 3 < pixels.Length; i += 4)
{
var key = (pixels[i + 2] << 16) | (pixels[i + 1] << 8) | pixels[i];
histogram[key] = histogram.TryGetValue(key, out var seen) ? seen + 1 : 1;
}
var background = histogram.OrderByDescending(pair => pair.Value).First().Key;
int bgR = (background >> 16) & 0xFF, bgG = (background >> 8) & 0xFF, bgB = background & 0xFF;
if (Spread(bgR, bgG, bgB) > ChannelSpread)
{
return (0, 0, $"{x},{y} 배경이 유채색({bgR},{bgG},{bgB}) — 판정 불가");
}
var fringed = 0;
var ink = 0;
for (var i = 0; i + 3 < pixels.Length; i += 4)
{
int b = pixels[i], g = pixels[i + 1], r = pixels[i + 2];
// 배경 그 자체는 세지 않는다 — 글리프가 만든 픽셀만 본다
if ((r == bgR && g == bgG && b == bgB) is false && Spread(r, g, b) > ChannelSpread)
{
fringed++;
}
// Rec.601 근사 — 정수만으로 충분하다
if ((r * 299 + g * 587 + b * 114) / 1000 < InkLuma)
{
ink++;
}
}
return (fringed, ink, $"{x},{y} {width}x{height} 배경({bgR},{bgG},{bgB})");
}
/// <summary>데스크톱 DC 에서 사각형 하나를 떠 BGRA 바이트로 돌려준다</summary>
private static byte[]? Grab(int x, int y, int width, int height)
{
var screen = GetDC(IntPtr.Zero);
if (screen == IntPtr.Zero)
{
return null;
}
var memory = IntPtr.Zero;
var bitmap = IntPtr.Zero;
try
{
memory = CreateCompatibleDC(screen);
bitmap = CreateCompatibleBitmap(screen, width, height);
if (memory == IntPtr.Zero || bitmap == IntPtr.Zero)
{
return null;
}
var previous = SelectObject(memory, bitmap);
// SRCCOPY — 합성이 끝난 화면을 그대로 가져온다
if (!BitBlt(memory, 0, 0, width, height, screen, x, y, 0x00CC0020))
{
return null;
}
SelectObject(memory, previous);
var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
bitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
var converted = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);
var stride = converted.PixelWidth * 4;
var buffer = new byte[stride * converted.PixelHeight];
converted.CopyPixels(buffer, stride, 0);
return buffer;
}
finally
{
if (bitmap != IntPtr.Zero)
{
DeleteObject(bitmap);
}
if (memory != IntPtr.Zero)
{
DeleteDC(memory);
}
ReleaseDC(IntPtr.Zero, screen);
}
}
[DllImport("user32.dll")] private static extern IntPtr GetDC(IntPtr hwnd);
[DllImport("user32.dll")] private static extern int ReleaseDC(IntPtr hwnd, IntPtr dc);
[DllImport("gdi32.dll")] private static extern IntPtr CreateCompatibleDC(IntPtr dc);
[DllImport("gdi32.dll")] private static extern IntPtr CreateCompatibleBitmap(IntPtr dc, int width, int height);
[DllImport("gdi32.dll")] private static extern IntPtr SelectObject(IntPtr dc, IntPtr obj);
[DllImport("gdi32.dll")] private static extern bool DeleteObject(IntPtr obj);
[DllImport("gdi32.dll")] private static extern bool DeleteDC(IntPtr dc);
[DllImport("gdi32.dll")] private static extern bool BitBlt(IntPtr dest, int x, int y, int width, int height,
IntPtr source, int sourceX, int sourceY, uint rop);
#endregion
}
+16 -2
View File
@@ -557,7 +557,17 @@
<Border.Resources>
<Style TargetType="ComboBoxItem" BasedOn="{StaticResource B.ComboItem}" />
</Border.Resources>
<ScrollViewer MaxHeight="260"><ItemsPresenter /></ScrollViewer>
<!-- 안쪽에 불투명 배경을 한 겹 더 두고 여기에 ClearTypeHint 를 건다.
Effect 와 AllowsTransparency 는 각각 중간 서피스를 만들어 ClearType 을 끄고,
그러면 13px 한글이 회색 안티에일리어싱으로 뭉개진다.
힌트를 Effect 가 걸린 바깥 Border 에 걸면 듣지 않는다.
중간 서피스 안쪽의 불투명 배경에 걸어야 한다(진단 cleartype 으로 잰 결과). -->
<ScrollViewer MaxHeight="260">
<Grid Background="{DynamicResource B.Surface}"
RenderOptions.ClearTypeHint="Enabled">
<ItemsPresenter />
</Grid>
</ScrollViewer>
</Border>
</Popup>
</Grid>
@@ -1096,8 +1106,12 @@
</Border>
<Popup x:Name="pop" Placement="Bottom" IsOpen="{TemplateBinding IsSubmenuOpen}" AllowsTransparency="True"
Focusable="False" PopupAnimation="Fade">
<!-- AllowsTransparency 팝업은 레이어드 창이라 ClearType 이 꺼진다.
불투명 배경이 있는 이 자리에 힌트를 걸어 되살린다 —
메뉴에 배치·정렬 명령이 다 들어 있어 가장 많이 읽는 글자다. -->
<Border Background="{DynamicResource B.Surface}" BorderBrush="{DynamicResource B.Line}" BorderThickness="1"
CornerRadius="6" Padding="4" Margin="0,3,0,0">
CornerRadius="6" Padding="4" Margin="0,3,0,0"
RenderOptions.ClearTypeHint="Enabled">
<StackPanel IsItemsHost="True" />
</Border>
</Popup>
@@ -740,7 +740,10 @@
HorizontalOffset="-176" VerticalOffset="4">
<Border Background="{DynamicResource B.Surface}" BorderBrush="{DynamicResource B.Line2}"
BorderThickness="1" CornerRadius="8" Padding="10" Effect="{DynamicResource B.FloatShadow}">
<StackPanel Width="212">
<!-- Effect 와 레이어드 팝업이 각각 중간 서피스를 만들어 ClearType 을 끈다.
힌트는 Effect 가 걸린 바깥 Border 에 걸면 듣지 않으므로, 안쪽 불투명 배경에 건다. -->
<StackPanel Width="212" Background="{DynamicResource B.Surface}"
RenderOptions.ClearTypeHint="Enabled">
<TextBlock Text="배경" FontSize="11" Foreground="{DynamicResource B.Muted}" Margin="0,0,0,5"/>
<ItemsControl ItemsSource="{Binding BackgroundSwatches}"
ItemTemplate="{StaticResource ColorSwatchTemplate}">
+6 -2
View File
@@ -626,8 +626,11 @@
<Border.Effect>
<DynamicResource ResourceKey="B.FloatShadow"/>
</Border.Effect>
<!-- 힌트는 ScrollViewer <b>안쪽</b>이어야 한다 — ScrollViewer 도 중간 서피스를 열어
바깥에 걸면 무효다(진단 cleartype 의 I/J 표본이 그 차이다). -->
<ScrollViewer MaxHeight="440" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding Source={StaticResource PaletteGrouped}}" Margin="10,9,10,10">
<ItemsControl ItemsSource="{Binding Source={StaticResource PaletteGrouped}}" Margin="10,9,10,10"
Background="{DynamicResource B.Panel}" RenderOptions.ClearTypeHint="Enabled">
<!-- 그룹 세로 나열 + 그룹 내 타일 4열([200] 플라이아웃 치수) -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@@ -672,7 +675,8 @@
<Border.Effect>
<DynamicResource ResourceKey="B.FloatShadow"/>
</Border.Effect>
<StackPanel Margin="5">
<StackPanel Margin="5" Background="{DynamicResource B.Panel}"
RenderOptions.ClearTypeHint="Enabled">
<Button Style="{StaticResource FlyoutRow}" Command="{Binding ZoomInCommand}" Click="OnZoomRowClick">
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Text="Ctrl+휠↑" Foreground="{DynamicResource B.Muted}"