미리보기가 편집을 따라오고, 창이 하나만 뜬다
미리보기 레거시 호환 계획의 P0-2. 환자 값이 들어가기 <b>전에</b> 반드시 고쳐야 하는 항목이다. <b>① 창이 여러 장 쌓였다.</b> 모덜리스인데 누를 때마다 새로 만들어서, 각각이 서로 다른 시점의 서식을 들고 겹쳐 있었고 어느 것이 지금 것인지 화면으로 구분할 수 없었다. 환자 데이터가 들어가면 그건 개인정보 사고가 된다 — 환자를 B 로 바꿔도 옛 창이 A 를 계속 띄운다. 이제 하나만 뜨고, 이미 있으면 지금 문서로 갈아타 앞으로 온다. <b>② 생성자에서 한 번만 그렸다.</b> 컨트롤을 추가·삭제하거나 숨김·인쇄출력을 토글해도 창은 옛 그림을 계속 보여 줬다 — 저장 전에 결과를 확인하려고 여는 창이 <b>구조적으로 거짓말</b>을 하고 있었다. 페이지·컨트롤 추가/삭제는 즉시(CollectionChanged), 그 밖의 변경은 창을 누르는 순간(Activated) 다시 그린다. 즉시 갱신이 안 되는 변경이 남아 있으므로 새로 고침 버튼도 함께 뒀다 — 자동에 기대게 하지 않는다. 제목에 서식명을 넣는다. 창이 하나뿐이라 무엇을 보고 있는지 제목이 말해야 한다. <b>같은 함정에 세 번째로 걸렸다.</b> 단정이 52 → 41 로 나왔다 — 재빌드는 됐는데 새 자식이 아직 실체화되지 않아 요소 수가 오히려 줄어 보인 것이다. UpdateLayout 을 돌리고 세니 통과했다. WPF 시각 트리를 세는 검사는 <b>반드시</b> 레이아웃을 먼저 돌려야 한다. 진단(--dialog-shots)은 생성자를 직접 부른다 — 단일 인스턴스 등록을 건너뛰어야 샷마다 새 창을 찍을 수 있다. 앱 코드는 ShowFor 만 쓴다는 것을 주석에 못 박았다. 게이트: 테스트 298/298, --edit-smoke 0실패(신규 1), --dialog-shots 글자 2,651개 FAIL 0 대조군 4/4, --modal-check 0실패, --cleartype 11/11, --maxrect 0실패, --scale-budget 5/5, 빌드 경고 0, --db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
dbb3deeda3
commit
9e3cd088e2
@@ -340,6 +340,30 @@ public static class EditSmoke
|
|||||||
// 인쇄 필터가 캔버스로 새면 안 된다 — 편집 중인 것을 못 보면 고칠 수 없다
|
// 인쇄 필터가 캔버스로 새면 안 된다 — 편집 중인 것을 못 보면 고칠 수 없다
|
||||||
Check("미리보기: 캔버스는 자식을 그대로 다 보여 준다",
|
Check("미리보기: 캔버스는 자식을 그대로 다 보여 준다",
|
||||||
boxedPanel.Children.Count == 3, $"{boxedPanel.Children.Count} (기대 3)");
|
boxedPanel.Children.Count == 3, $"{boxedPanel.Children.Count} (기대 3)");
|
||||||
|
|
||||||
|
// 창이 편집을 따라오는가.
|
||||||
|
// 전에는 생성자에서 한 번만 그려서, 컨트롤을 추가·삭제해도 옛 그림을 계속 보여 줬다 —
|
||||||
|
// 저장 전에 결과를 확인하려고 여는 창이 구조적으로 거짓말을 하고 있었다.
|
||||||
|
var live = new Views.PreviewWindow(boxedVm)
|
||||||
|
{
|
||||||
|
Left = -10000, Top = -10000,
|
||||||
|
WindowStartupLocation = WindowStartupLocation.Manual,
|
||||||
|
ShowActivated = false, ShowInTaskbar = false,
|
||||||
|
};
|
||||||
|
live.Show();
|
||||||
|
// 레이아웃을 돌리고 센다 — 재빌드 직후에는 새 자식이 아직 실체화되지 않아
|
||||||
|
// 요소 수가 오히려 줄어 보인다(실제로 52 → 41 로 걸렸다). 세 번째 같은 함정이다.
|
||||||
|
int Pages()
|
||||||
|
{
|
||||||
|
live.UpdateLayout();
|
||||||
|
return CountVisuals(live);
|
||||||
|
}
|
||||||
|
var livePages = Pages();
|
||||||
|
boxedVm.AddControlAt("Label", new Point(300, 400));
|
||||||
|
var liveAfterAdd = Pages();
|
||||||
|
Check("미리보기: 컨트롤을 추가하면 창이 따라온다", liveAfterAdd > livePages,
|
||||||
|
$"요소 {livePages} → {liveAfterAdd} (안 늘면 생성자에서 한 번만 그린 것)");
|
||||||
|
live.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) 클릭 선택 — 컨트롤 중심(핸들 반경 밖)
|
// 2) 클릭 선택 — 컨트롤 중심(핸들 반경 밖)
|
||||||
|
|||||||
@@ -970,10 +970,9 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
new Views.PreviewWindow(CurrentDesigner)
|
// 창은 하나만 뜬다 — 여러 장이 겹치면 각각 다른 시점의 서식을 들고 있게 되고,
|
||||||
{
|
// 환자 데이터가 들어가면 옛 창이 옛 환자를 계속 띄운다.
|
||||||
Owner = Application.Current.MainWindow,
|
Views.PreviewWindow.ShowFor(CurrentDesigner, Application.Current.MainWindow);
|
||||||
}.Show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPrint()
|
private void OnPrint()
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<ToolBarTray DockPanel.Dock="Top">
|
<ToolBarTray DockPanel.Dock="Top">
|
||||||
<ToolBar>
|
<ToolBar>
|
||||||
<Button Content="인쇄..." Padding="10,3" Click="OnPrint"/>
|
<Button Content="인쇄..." Padding="10,3" Click="OnPrint"/>
|
||||||
|
<Button Content="새로 고침" Padding="10,3" Click="OnRefresh"
|
||||||
|
ToolTip="구조 변경은 자동으로 따라오고, 창을 누르면 다시 그립니다"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<Button Content="-" Padding="8,3" Click="OnZoomOut"/>
|
<Button Content="-" Padding="8,3" Click="OnZoomOut"/>
|
||||||
<TextBlock x:Name="ZoomText" Text="100%" VerticalAlignment="Center" Margin="6,0" MinWidth="42" TextAlignment="Center"/>
|
<TextBlock x:Name="ZoomText" Text="100%" VerticalAlignment="Center" Margin="6,0" MinWidth="42" TextAlignment="Center"/>
|
||||||
|
|||||||
@@ -1,29 +1,124 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Documents;
|
|
||||||
using SheetMe.Designer.Services;
|
using SheetMe.Designer.Services;
|
||||||
using SheetMe.Designer.ViewModels;
|
using SheetMe.Designer.ViewModels;
|
||||||
|
|
||||||
namespace SheetMe.Designer.Views;
|
namespace SheetMe.Designer.Views;
|
||||||
|
|
||||||
/// <summary>미리보기 창 — 편집 크롬 없는 페이지 렌더(인쇄와 동일 비주얼) + 줌/인쇄.</summary>
|
/// <summary>
|
||||||
|
/// 미리보기 창 — 편집 크롬 없는 페이지 렌더(인쇄와 동일 비주얼) + 줌/인쇄.
|
||||||
|
///
|
||||||
|
/// <b>창은 하나만 뜬다.</b> 전에는 누를 때마다 새 창이 생겼다. 모덜리스라 여러 장이 겹쳐 쌓이고,
|
||||||
|
/// 각각이 <b>서로 다른 시점의 서식</b>을 들고 있었다. 어느 것이 지금 것인지 화면으로 구분할 수 없다.
|
||||||
|
/// 환자 데이터가 들어가면 그건 개인정보 사고가 된다 — 환자를 바꿔도 옛 창이 옛 환자를 계속 띄운다.
|
||||||
|
///
|
||||||
|
/// <b>내용은 따라간다.</b> 전에는 생성자에서 한 번만 그려서, 컨트롤을 추가·삭제하거나
|
||||||
|
/// 숨김·인쇄출력을 토글해도 창은 옛 그림을 계속 보여 줬다 —
|
||||||
|
/// 저장 전에 결과를 확인하려고 여는 창이 <b>구조적으로 거짓말</b>을 하고 있었다.
|
||||||
|
/// 페이지·컨트롤 추가/삭제는 즉시, 그 밖의 변경은 창을 누르는 순간(Activated) 다시 그린다.
|
||||||
|
/// 즉시 갱신이 안 되는 변경이 남아 있으므로 <b>새로 고침 버튼</b>도 함께 둔다.
|
||||||
|
/// </summary>
|
||||||
public partial class PreviewWindow : Window
|
public partial class PreviewWindow : Window
|
||||||
{
|
{
|
||||||
#region Member Fields
|
#region Member Fields
|
||||||
private readonly DesignerViewModel designer;
|
/// <summary>지금 떠 있는 창 — 두 번째를 만들지 않는다</summary>
|
||||||
|
private static PreviewWindow? open;
|
||||||
|
|
||||||
|
private DesignerViewModel designer;
|
||||||
private double zoom = 1.0;
|
private double zoom = 1.0;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
public PreviewWindow(DesignerViewModel designer)
|
/// <summary>
|
||||||
|
/// 진단(--dialog-shots)만 직접 부른다 — 앱 코드는 <see cref="ShowFor"/> 를 쓸 것.
|
||||||
|
/// 여기로 만들면 단일 인스턴스 등록을 건너뛰므로 두 장이 생긴다.
|
||||||
|
/// </summary>
|
||||||
|
internal PreviewWindow(DesignerViewModel designer)
|
||||||
{
|
{
|
||||||
this.designer = designer;
|
this.designer = designer;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
Activated += (_, _) => BuildPages();
|
||||||
|
Closed += (_, _) =>
|
||||||
|
{
|
||||||
|
Unsubscribe();
|
||||||
|
if (ReferenceEquals(open, this))
|
||||||
|
{
|
||||||
|
open = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Subscribe();
|
||||||
BuildPages();
|
BuildPages();
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Methods
|
#region Methods
|
||||||
|
/// <summary>
|
||||||
|
/// 미리보기를 보여 준다 — 이미 떠 있으면 그 창을 <b>지금 문서로 바꿔</b> 앞으로 가져온다.
|
||||||
|
/// 새 창을 하나 더 만들면 어느 것이 지금 것인지 알 수 없다.
|
||||||
|
/// </summary>
|
||||||
|
public static void ShowFor(DesignerViewModel designer, Window? owner)
|
||||||
|
{
|
||||||
|
if (open is null)
|
||||||
|
{
|
||||||
|
open = new PreviewWindow(designer) { Owner = owner };
|
||||||
|
open.Show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
open.Retarget(designer);
|
||||||
|
if (open.WindowState == WindowState.Minimized)
|
||||||
|
{
|
||||||
|
open.WindowState = WindowState.Normal;
|
||||||
|
}
|
||||||
|
open.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>다른 문서로 갈아탄다 — 탭을 바꾸고 미리보기를 다시 누른 경우</summary>
|
||||||
|
private void Retarget(DesignerViewModel next)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(designer, next))
|
||||||
|
{
|
||||||
|
BuildPages();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Unsubscribe();
|
||||||
|
designer = next;
|
||||||
|
Subscribe();
|
||||||
|
BuildPages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>페이지·컨트롤 추가/삭제는 즉시 따라간다</summary>
|
||||||
|
private void Subscribe()
|
||||||
|
{
|
||||||
|
designer.Pages.CollectionChanged += OnStructureChanged;
|
||||||
|
foreach (var page in designer.Pages)
|
||||||
|
{
|
||||||
|
page.Controls.CollectionChanged += OnStructureChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unsubscribe()
|
||||||
|
{
|
||||||
|
designer.Pages.CollectionChanged -= OnStructureChanged;
|
||||||
|
foreach (var page in designer.Pages)
|
||||||
|
{
|
||||||
|
page.Controls.CollectionChanged -= OnStructureChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStructureChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
// 페이지가 늘거나 줄면 그 페이지의 컨트롤 구독도 다시 걸어야 한다
|
||||||
|
if (ReferenceEquals(sender, designer.Pages))
|
||||||
|
{
|
||||||
|
foreach (var page in designer.Pages)
|
||||||
|
{
|
||||||
|
page.Controls.CollectionChanged -= OnStructureChanged;
|
||||||
|
page.Controls.CollectionChanged += OnStructureChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BuildPages();
|
||||||
|
}
|
||||||
|
|
||||||
private void BuildPages()
|
private void BuildPages()
|
||||||
{
|
{
|
||||||
PagesHost.Children.Clear();
|
PagesHost.Children.Clear();
|
||||||
@@ -41,8 +136,13 @@ public partial class PreviewWindow : Window
|
|||||||
};
|
};
|
||||||
PagesHost.Children.Add(frame);
|
PagesHost.Children.Add(frame);
|
||||||
}
|
}
|
||||||
|
Title = designer.Document.Title.Length > 0
|
||||||
|
? $"미리보기 — {designer.Document.Title}"
|
||||||
|
: "미리보기";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnRefresh(object sender, RoutedEventArgs e) => BuildPages();
|
||||||
|
|
||||||
private void OnPrint(object sender, RoutedEventArgs e)
|
private void OnPrint(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
Reference in New Issue
Block a user