Programowanie w technologii .NET
wykład 9 – Dokumenty
Kontrolki Label i TextBlock nie nadają się do wyświetlania dużej ilości formatowanego tekstu (np. dokumentów czy pomocy):
• Dokumenty pozwalają prezentować dużą ilość tekstu w czytelny, wygodny sposób:
obsługa kolumn, podziału na strony, dzielenia słów, ...
Dokumenty w WPF dzielą się na dwie kategorie:
• Fixed documents – dokumenty przeznaczone do wydruku, o ustalonej pozycji całej zawartości, stronicowaniu; są odpowiednikiem plików PDF. Ważne, gdy chcemy wydrukować coś bez zmian.
• Flow documents – przeznaczone do oglądania w okienku, ich zawartość układana jest automatycznie – dostosowując się do okna i sposobu wyświetlania;
odpowiadają dokumentom HTML.
Dokument musi być wyświetlany w kontenerze:
• Fixed documents – w kontenerze DocumentViewer
• Flow documents – do dyspozycji mamy kontenery FlowDocumentReader, FlowDocumentPageViewer i FlowDocumentScrollViewer
Wszystkie te kontenery są tylko do odczytu. Poza tym, mamy do dyspozycji API do tworzenia fixed documents oraz kontrolkę RichTextBox do edycji flow documents.
Flow Documents
• W tym typie dokumentów zawartość dostosowuje się do kontenera. Przeznaczone do wyświetlania na ekranie.
• Jest pozbawiony wielu wad HTMLa. Domyślnie zawartość HTML wypełnia okno – co może utrudniać czytanie, jeśli szerokość okienka (i długość linii) jest duża.
Rozwiązaniem jest odgórne ograniczenie szerokości, ale wiąże się to ze stratą miejsca (marginesy).
• Flow Document dodaje m. in. paginację (podział na strony), formaty
wielokolumnowe, dzielenie słów i możliwość zmiany ustawień użytkownika.
Do tworzenia dokumentu typu flow służy klasa FlowDocument.
Nowy dokument możemy tworzyć jako osobny plik lub też umieścić go w
istniejącym oknie, wykorzystując jeden z dostępnych kontenerów (w przykładzie użyjmy FlowDocumentScrollViewer).
<Window ...>
<FlowDocumentScrollViewer>
<FlowDocument>
...
</FlowDocument>
</FlowDocumentScrollViewer>
</Window>
Flow Elements
• Nie możemy umieścić tekstu bezpośrednio w dokumencie. Flow document tworzony jest z flow elements.
• Nie dziedziczą one z klas UIElement i FrameworkElement, lecz tworzą własną, odrębną hierarchię klas ContentElement i FrameworkContentElement.
• Są prostsze, niż elementy poznawane dotąd, ale obsługują podobny zestaw zdarzeń i własności.
• Podstawowa różnica: nie odpowiadają za renderowanie samych siebie. Zajmuje się tym kontener, który je przechowuje (co pozwala na optymalizację).
• Domyślnie nie przyjmują też focusa (Focusable ustawione na false).
Dwa podstawowe typy Flow Elements:
• Block elements (elementy blokowe) – służą do grupowania innych elementów. Np.
paragraf (Paragraph) może zawierać kilka sekcji różnie sformatowanego tekstu.
• Inline elements (elementy liniowe) – są zagnieżdżane w elementach blokowych (lub innych elementach liniowych). Np. element Run zawiera pewien tekst, który możemy umieścić w paragrafie.
Ten model zawartości pozwala na wielokrotne poziomy zagnieżdżenia (np. element Bold w elemencie Underline – model bardzo podobny do HTMLa)
Elementem najwyższego poziomu musi być element blokowy (np. Paragraph):
<Window ...>
<FlowDocumentScrollViewer>
<FlowDocument>
<Paragraph>Hello, world!</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>
</Window>
Nie ma ograniczenia, ilu elementów najwyższego poziomu użyjemy:
<Window ...>
<FlowDocumentScrollViewer>
<FlowDocument>
<Paragraph>Hello, world!</Paragraph>
<Paragraph>A to jest drugi paragraf.</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>
</Window>
Pasek przewijania jest dodawany automatycznie, a czcionka jest pobrana z ustawień systemowych, a nie okna zawierającego kontener.
Domyślnie FlowDocumentScrollViewer.IsSelectionEnabled jest ustawione na true.
Formatowanie elementów
• Foreground, Background – pędzle wypełniające pierwszy plan i tło elementu
• FontFamily, FontSize, FontStretch, FontStyle, FontWeight – własności dotyczące kroju pisma
• ToolTip – podpowiedź wyświetlana po zatrzymaniu się na elemencie
• Style – styl definiujący wygląd elementu
• BorderBrush, BorderThickness – obramowanie elementu
• Margin – odległość między elementem a kontenerem lub elementem sąsiednim.
Domyślnie odległość między Block elements (np. dwoma paragrafami) to 18 jednostek (jeśli chcemy to zmniejszyć, należy zmniejszać z obu stron).
• Padding – odstęp między krawędzią elementu a elementem zagnieżdżonym
• TextAlignment – wyrównanie w poziomie (Left, Right, Center lub Justify)
• LineHeight – odległość między liniami zagnieżdżonego tekstu.
• LineStackingStrategy – określa, w jaki sposób linie są oddzielone, gdy zawierają tekstu o różnych wielkościach czcionki (MaxHeight wybiera najwyższy rozmiar czcionki, BlockLineHeight używa wartości LineHeight dla wszystkich linii) Ponadto:
• TextDecorations (udostępniane przez Paragraph i wszystkie elementy inline) – Strikethrough, Overline, Underline lub dowolna ich kombinacja.
• Typography – szczegóły dotyczące sposobu renderowania tekstu.
Elementy blokowe Paragraph i Run
Paragraf nie zawiera tekstu, ale kolekcję elementów liniowych – Paragraph.Inlines (może on zatem zawierać nie tylko tekst).
Aby umieścić tekst w paragrafie, musi znaleźć się on w elemencie Run:
<Paragraph>
<Run>A to jest drugi paragraf.</Run>
</Paragraph>
Jeśli go nie podamy, zostanie utworzony automatycznie, ale należy o tym pamiętać, jeśli chcemy sięgnąć do tego tekstu programistycznie:
<Paragraph Name="paragraf">Hello, world!</Paragraph>
((Run)paragraf.Inlines.FirstInline).Text = "Dzień dobry!";
Lepszym pomysłem może być umieszczanie tekstu, który chcemy modyfikować w elemencie Span (aby móc go nazwać i sięgać do niego bezpośrednio).
Klasa Paragraph zawiera własność TextIndent, która pozwala ustawić rozmiar wcięcia pierwszego wiersza.
W odróżnieniu od HTML, w WPF nie ma elementów nagłówków (należy używać odpowiednio sformatowanych paragrafów).
List
Reprezentuje punktowaną lub numerowana listę – wybrać to można przy pomocy własności MarkerStyle:
• Disc
• Box
• Circle
• Square
• Decimal (domyślnie zaczyna się od 1, ale można wybrać inny StartingIndex)
• LowerLatin (a, b, c).
• UpperLatin (A, B, C).
• LowerRoman (i, ii, iii, iv).
• UpperRoman (I, II, III, IV).
• None – bez znacznika
MarkerOffset pozwala ustawić odstęp między znacznikiem a elementem listy.
Wewnątrz listy zagnieżdżamy elementy ListItem, reprezentujące poszczególne podpunkty.
Każdy ListItem musi zawierać element blokowy (np. Paragraph).
<FlowDocument>
<Paragraph>Tematy projektów</Paragraph>
<List>
<ListItem>
<Paragraph>Sklep</Paragraph>
</ListItem>
<ListItem>
<Paragraph>Miejski Dom Kultury</Paragraph>
</ListItem>
<ListItem>
<Paragraph>Lista zadań</Paragraph>
</ListItem>
<ListItem>
<Paragraph>Wizard klas</Paragraph>
</ListItem>
</List>
</FlowDocument>
Table
Przeznaczona do wyświetlania zawartości w postaci tabeli. Jest wzorowana na <table> z HTMLa.
Aby stworzyć tabelę, należy:
1. Umieścić element TableRowGroup w elemencie Table. TableRowGroup zawiera grupę wierszy. Pozwala nadać jedno formatowanie kilku wierszom jednocześnie.
2. Umieścić element TableRow w elemencie TableRowGroup. TableRow reprezentuje pojedynczy wiersz.
3. W TableRow umieścić element TableCell dla każdej kolumny wiersza.
4. W TableCell umieścić element blokowy (przeważnie Paragraph), do przechowywania zawartości komórki.
<FlowDocument>
<Paragraph FontSize="16pt">Oddanych zadań:</Paragraph>
<Table>
<TableRowGroup Paragraph.TextAlignment="Center">
<TableRow FontWeight="Bold">
<TableCell>
<Paragraph>Nr</Paragraph>
</TableCell>
<TableCell>
<Paragraph>Temat</Paragraph>
</TableCell>
<TableCell>
<Paragraph>Oddanych</Paragraph>
</TableCell>
</TableRow>
...
</TableRowGroup>
</Table>
</FlowDocument>
• W przeciwieństwie do kontenera Grid, komórki w Table są wypełniane w kolejności (wg pozycji).
• Domyślnie kolumny rozmieszczane są równomiernie. Można samodzielnie określić rozmiar lub proporcje (ale nie oba naraz, nie możemy łączyć kolumn o ustalonym rozmiarze z proporcjonalnymi):
<Table.Columns>
<TableColumn Width="*"/>
<TableColumn Width="3*"/>
<TableColumn Width="*"/>
</Table.Columns>
• Można również określić ColumnSpan lub/i RowSpan.
• CellSpacing określa odstęp między komórkami.
• Każda komórka może być formatowana osobno.
• Nie ma zbyt dobrego wsparcia dla obramowań: istnieją BorderThickness, BorderBrush (własności TableCell), ale rysują osobną krawędź wokół każdej komórki; BorderThickness i BorderBrush w klasie Table to krawędź wokół całej tabeli.
Section
Sekcja jest analogiczna do elementu <div> z HTMLa.
Służy do grupowania elementów blokowych, by móc nadać im jeden styl formatowania.
<Section FontFamily="Comic Sans MS"
Background="LightSteelBlue">
<Paragraph > ... </Paragraph>
<Paragraph > ... </Paragraph>
</Section>
(Oczywiście można też używać w połączeniu ze stylami.)
(działa to dla czcionek, bo są dziedziczone, a dla tła, bo domyślnie w paragrafie jest przezroczyste i widać tło sekcji)
BlockUIContainer
• Pozwala na umieszczenie w dokumencie (w miejscu elementu blokowego) elementów dziedziczących z UIElement.
• Pozwala na dodawanie np. przycisków, pól wyboru, a nawet całych paneli czy gridów ze złożoną zawartością. Jedyne ograniczenie: pojedyncze dziecko.
• Jest to przydatne, gdy chcemy połączyć dokument z kontrolkami zapewniającymi pewną interakcję z użytkownikiem, np. przyciski w systemie pomocy lub pola tekstowe w formularzu.
<Paragraph>
Naciśnij, aby uzyskać więcej pomocy.
</Paragraph>
<BlockUIContainer>
<Button HorizontalAlignment="Left" Padding="5">
Pomoc </Button>
</BlockUIContainer>
Elementy liniowe
• Run – zawiera zwykły tekst, można ustawić mu formatowanie, choć częściej stosuje się do tego Span; tworzony domyślnie np. w paragrafach
• Span – (analogiczne do HTMLowego <span>) opakowuje dowolną liczbę elementów inline, zazwyczaj używany do formatowania fragmentu tekstu (w tym celu opakowujemy w niego tekstu lub np. element Run); przydatne do nazwania fragmentu dokumentu (aby móc odwołać się do niego w kodzie)
• Bold, Italic, and Underline – umożliwiają nałożenie odpowiedniego formatowania na tekst (zazwyczaj lepiej jest jednak opakować tekst w Span i ustawić odpowiedni styl)
• Hyperlink – reprezentuje odnośnik (podnosi zdarzenie Click)
• LineBreak – dodaje złamanie linii
• InlineUIContainer – pozwala na umieszczanie elementów dziedziczących po UIElement w miejscu elementów inline.
• Floater, Figure – umożliwia wstawienie specjalnie traktowanego obszaru do wyświetlania obrazów, wyróżnionych informacji i innej zawartości
Białe znaki
Na początku lub końcu elementu są ignorowane, ale przed elementem inline – nie. Dlatego to działa źle:
<Paragraph>Do końca grudnia<Bold> należy </Bold>przygotować prototyp interfejsu.</Paragraph>
a to dobrze:
<Paragraph>Do końca grudnia <Bold>należy</Bold> przygotować prototyp interfejsu.</Paragraph>
Można też używać:
<Run xml:space="preserve"> a b c </Run>
Floater
Element, który pozwala umieścić pewną zawartość obok głównego dokumentu:
<Paragraph>
Przykładowy tekst.
<Floater Style="{StaticResource Cytat}">
<Paragraph>...cytat...</Paragraph>
</Floater>
Dalszy ciąg przykładowego tekstu..
</Paragraph>
<Paragraph>
Kolejny akapit.
</Paragraph>
Styl użyty w przykładzie:
<Style x:Key="Cytat">
<Setter Property="Paragraph.FontSize" Value="24"/>
<Setter Property="Paragraph.FontStyle" Value="Italic"/>
<Setter Property="Paragraph.Foreground" Value="SteelBlue"/>
<Setter Property="Paragraph.Padding" Value="5"/>
<Setter Property="Paragraph.Margin" Value="15,10,5,10"/>
</Style>
Możemy ograniczyć zajmowane przez niego miejsce i ręcznie określić położenie:
<Floater Style="{StaticResource Cytat}" Width="200"
HorizontalAlignment="Left">
<Paragraph>...</Paragraph>
</Floater>
Aby dostosować wygląd, możemy też ustawić własności Background, BorderBrush, BorderThickness, Margin i Padding (pozostałe elementy inline tego nie mają, tylko Floater i Figure).
Możemy go użyć do wyświetlenia obrazka (element Image wewnątrz BlockUIContainer lub InlineUIContainer). Niestety, Floater nie dostosowuje szerokości do zawartości.
<Paragraph>...</Paragraph>
<Paragraph>
...
<Floater Width="100" Padding="5,0,5,0"
HorizontalAlignment="Right">
<BlockUIContainer>
<Image Source="Masqueofthereddeath.jpg"></Image>
</BlockUIContainer>
</Floater>
...
</Paragraph>
Figure
Podobna do Floatera, ale daje więcej kontroli nad położeniem.
uwaga: wiele poniższych własności (np. HorizontalAnchor, VerticalOffset,
HorizontalOffset) nie jest wspieranych przez kontener FlowDocumentScrollViewer, dlatego w przykładzie użyjemy FlowDocumentReader.
• Width – szerokość; poza stała wartością, jak w Floaterze, możemy podać wartość proporcjonalną (np. „0.25 content”, „2 Column”).
• Height – wysokość – Floater dostosowywał ją do zawartości, tu możemy określić ją ręcznie
• HorizontalAnchor – zamiast HorizontalAlignment z klasy Floatera; pozwala orientować również względem strony lub kolumny (poza np. ContentCenter mamy PageCenter i ColumnCenter).
• VerticalAnchor – pozwala wyrównać do bieżącej linii, kolumny, strony
• HorizontalOffset, VerticalOffset – pozwalają na przesunięcie figury względem miejsca zakotwiczenia
• WrapDirection – określa, czy tekst może opływać figurę z jednej czy obu stron
<FlowDocumentReader>
<FlowDocument>
<Paragraph>...</Paragraph>
<Paragraph>...
<Figure Width="0.5 column" Padding="5,0,5,0"
HorizontalAnchor="ColumnLeft">
<BlockUIContainer>
<Image Source="Masqueofthereddeath.jpg" />
</BlockUIContainer>
</Figure>
...</Paragraph>
</FlowDocument>
</FlowDocumentReader>
Interakcja z elementami
(uwaga: tekst musimy ręcznie umieszczać w elementach Run) // Pierwszy fragment zdania
Run runFirst = new Run();
runFirst.Text = "Oto ";
// Wytłuszczony tekst Bold bold = new Bold();
Run runBold = new Run();
runBold.Text = "dynamicznie wygenerowany";
bold.Inlines.Add(runBold);
// Koniec zdania
Run runLast = new Run();
runLast.Text = " dokument.";
// Dodawanie fragmentów do paragrafu Paragraph paragraph = new Paragraph();
paragraph.Inlines.Add(runFirst);
paragraph.Inlines.Add(bold);
paragraph.Inlines.Add(runLast);
// Utworzenie dokumentu i dodanie paragrafu FlowDocument document = new FlowDocument();
document.Blocks.Add(paragraph);
// Wyświetlenie dokumentu
docViewer.Document = document;
A czego użyć, aby przeglądać (i modyfikować) gotowy dokument?
• kolekcja FlowDocument.Blocks zwraca elementy blokowe;
FlowDocument.Blocks.FirstBlock zwraca pierwszy, a FlowDocument.Blocks.LastBlock – ostatni element kolekcji
• w celu przeglądania elementów blokowych: Block.NextBlock (lub
Block.PreviousBlock). Ponadto kolekcja Block.SiblingBlocks pozwala przeglądać elementy tego samego poziomu co aktualny
• wiele elementów blokowych zawiera inne elementy (np. List zawiera elementy ListItem, Section zawiera kolekcję Blocks, Paragraph zawiera Inlines.
Jeśli celem jest wymienienie fragmentu tekstu w dokumencie, najlepiej jest wydzielić ten fragment w postaci elementu Span. Możemy rozpoznawać je po nazwie, albo np. po Tagu (jeśli szukamy elementu, który występuje więcej niż raz).
Justowanie
• Domyślnie dokumenty są wyjustowane (TextAlignment – Justify).
• Ustawienie FlowDocument.IsOptimalParagraphEnabled na true włącza opcję optymalnego dzielenia wierszy (podziały linii są ustawiane tak, by jak najlepiej rozłożyć odstępy w dokumencie)
Ustawiając
FlowDocument.IsHyphenationEnabled na true włączamy dzielenie słów (do ustalenia podziału wykorzystywany jest słownik).
Kontenery Flow Document
Są tylko do odczytu. Wszystkie obsługują drukowanie i zoom.
• FlowDocumentScrollViewer – cały dokument z paskiem przewijania. Nie obsługuje paginacji, ani wielu kolumn.
• FlowDocumentPageViewer – dzieli dokument na wiele stron.
• FlowDocumentReader – łączy cechy obu powyższych: użytkownik wybiera który rodzaj widoku go interesuje.
Zmiana kontenera jest prosta:
<FlowDocumentPageViewer>
<FlowDocument>
<Paragraph>Jeden akapit tekstu.</Paragraph>
</FlowDocument>
</FlowDocumentPageViewer>
Jak prosty kontener może też służyć TextBlock – może przechowywać elementy liniowe (inline), oferuje zawijanie (TextWrapping) i ucinanie (TextTrimming) tekstu. To ostatnie może przyjmować wartość: None, WordEllipse (niemieszczące się słowa zastępuje „...”), CharacterEllipse (niemieszczące się znaki zastępuje „...”).
Zoom
• Ustawiany przy pomocy własności Zoom kontenera. Jest to wartość w procentach.
• Można ustawiać ją ręcznie, używać metod IncreaseZoom() i DecreaseZoom() (zwiększają lub zmniejszają o ZoomIncrement) lub pozwolić wybrać
użytkownikowi (FlowDocumentScrollViewer daje do dyspozycji suwak powiększenia):
<FlowDocumentScrollViewer MinZoom="50" MaxZoom="1000"
Zoom="100" ZoomIncrement="5" IsToolBarVisible="True">
...
</FlowDocumentScrollViewer>
Elementy Floater lub Figure o ustalonym rozmiarze też są powiększane proporcjonalnie.
Strony i kolumny
• Kontener FlowDocumentPageViewer może dzielić długi dokument na strony – zastępuje to scrollowanie.
• A jeśli okno będzie dość szerokie, tekst zostanie podzielony na kolumny.
Uwaga: Floater domyślnie przyjmuje szerokość całej kolumny, Można go zmniejszyć, ale nie powiększyć. Z kolei Figure może zajmować kilka kolumn.
• FlowDocumentReader również obsługuje podział na strony: można wybrać widok jednej strony (jak we FlowDocumentPageViewer) lub dwóch stron obok siebie.
Podział na strony i kolumny możemy kontrolować w klasie FlowDocument lub Paragraph.
FlowDocument:
• ColumnWidth – preferowany rozmiar kolumny (działa jako rozmiar minimalny)
• IsColumnWidthFlexible – decyduje, czy kontener może dopasować rozmiar kolumny (jeśli false, użyta jest dokładna wartość ColumnWidth, jeśli true – ColumnWidth to rozmiar minimalny). Domyślnie: true;
• ColumnGap – odstęp między kolumnami
• ColumnRuleWidth i ColumnRuleBrush – szerokość i wypełnienie pionowej kreski między kolumnami
Paragraph:
• KeepTogether – czy paragraf może być podzielony na strony (gdy true: cały paragraf znajdzie się na jednej stronie)
• KeepWithNext – czy można oddzielić końcem strony ten paragraf od następnego (przeważnie: do nagłówków)
• MinOrphanLines – jak paragraf jest dzielony na strony; minimalna liczba linii, jaka ma pozostać na poprzedniej stronie (gdy się nie mieszczą – przenoszony jest cały paragraf)
• MinWidowLines – minimalna liczba linii, jaka ma być przeniesiona na następną stronę
Odczyt dokumentu z pliku
• Do ładowania zawartości do kontenera służy klasa XamlReader (z
System.Windows.Markup). W poniższym kodzie pominięto obsługę błędów:
using (FileStream fs = File.Open(documentFile, FileMode.Open)) { FlowDocument document = XamlReader.Load(fs) as FlowDocument;
if (document == null) {
MessageBox.Show("Nie udało się odczytać dokumentu.");
} else {
flowContainer.Document = document;
} }
• Podobnie łatwo zapisać dokument z kontenera:
using (FileStream fs = File.Open(documentFile, FileMode.Create)) {
XamlWriter.Save(flowContainer.Document, fs);
}
Drukowanie
• Wystarczy wywołać metodę Print() z kontenera. Wyświetla on okno drukowania z wyborem drukarki i ustawień.
• Drukowanie działa przez system poleceń, zatem wystarczy dodać przycisk (lub skorzystać z Ctrl+P):
<Button Command="ApplicationCommands.Print"
CommandTarget="docViewer">Print</Button>
W podobny sposób obsługiwane jest np. wyszukiwanie i powiększenie.
Edycja Flow Document
Kontrolka RichTextBox umożliwia edycję flow documents.
Ładowanie pliku – można zadeklarować dokument od razu wewnątrz RichTextBox:
<RichTextBox>
<FlowDocument>
<Paragraph>Dokument do edycji.</Paragraph>
</FlowDocument>
</RichTextBox>
Można też wczytać go przy pomocy
XamlReader.Load(). Jeśli chcemy czytać inne formaty, pomocna jest klasa
System.Windows.Documents.TextRange.
Dozwolone typy:
• DataFormat.Xaml – zawartość XAML
• DataFormats.Rtf – rich text (*.rtf)
• DataFormats.XamlPackage – zawartość XAML z osadzonymi obrazami
• DataFormats.Text – zwykły tekst
OpenFileDialog openFile = new OpenFileDialog();
openFile.Filter = "RichText Files (*.rtf)|*.rtf|All Files (*.*)|
*.*";
if (openFile.ShowDialog() == true) {
TextRange documentTextRange = new TextRange(
richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
using (FileStream fs = File.Open(openFile.FileName, FileMode.Open)) {
// po rozszerzeniu rozpoznajemy typ pliku:
if (Path.GetExtension(openFile.FileName).ToLower() == ".rtf") {
documentTextRange.Load(fs, DataFormats.Rtf);
} else {
documentTextRange.Load(fs, DataFormats.Xaml);
} } }
(najpierw tworzymy TextRange obejmujący zawartość dokumentu, którą chcemy wymienić)
Zapis pliku:
SaveFileDialog saveFile = new SaveFileDialog();
saveFile.Filter =
"XAML Files (*.xaml)|*.xaml|RichText Files (*.rtf)|*.rtf|All Files (*.*)|*.*";
if (saveFile.ShowDialog() == true) {
// cały zakres dokumentu:
TextRange documentTextRange = new TextRange(
richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
// Jeśli plik istnieje, zostanie nadpisany
using (FileStream fs = File.Create(saveFile.FileName)) {
if (Path.GetExtension(saveFile.FileName).ToLower() == ".rtf") {
documentTextRange.Save(fs, DataFormats.Rtf);
} else {
documentTextRange.Save(fs, DataFormats.Xaml);
} } }
Elementem najwyższego poziomu będzie Section (tylko takie pliki XAML są akceptowane).
Formatowanie tekstu: Kontrolka RichTextBox obsługuje polecenia związane z edycją i formatowaniem dokumentów, dlatego aby obsługiwać np. zmiany formatu, wystarczy dodać przyciski na pasku narzędzi.
Fixed Documents
• Fixed documents są mnie elastyczne niż Flow documents.
• Służą jako dokument gotowy do wydruku, który można przenosić i drukować w niezmienny formacie (na wzór PDF).
• Wykorzystują pliki XPS (XML Paper Specification).
• Sterowniki drukarki w Viście i Windows 7 pozwalają na drukowanie dowolnych dokumentów do formatu XPS.
• Format XPF to archiwum zip zawierające dokument w postaci XML, wraz z obrazkami, osadzonymi czcionkami, etc.
Dokument XPS możemy wyświetlić w kontenerze DocumentViewer.
XpsDocument doc = new XpsDocument("filename.xps", FileAccess.Read);
docViewer.Document = doc.GetFixedDocumentSequence();
doc.Close();
W dokumentach XPS układ i rozmiar strony są ustalone, dokument przeznaczony jest od razu do wydruku.
Annotations
Adnotacje. Pozwalają dodać komentarz lub wyróżnić fragment tekstu w dokumencie flow lub fixed.
Dwa rodzaje:
• Highlighting – wybranie części tekstu i wyróżnienie go kolorem (zakreślacz).
• Sticky notes – dołączenie do fragmentu tekstu pływającej ramki z informacją.
• Wszystkie cztery rodzaje kontenerów obsługują adnotacje.
• Przedtem jednak musimy wykonać dwa kroki: włączyć możliwość stosowania adnotacji i dodać kontrolki, przy pomocy których użytkownik będzie je dodawał.
Klasy adnotacji
• AnnotationService – klasa zajmująca się usługą adnotacji (aby je udostępnić, należy stworzyć jej obiekt)
• AnnotationStore – zajmuje się składowanie dodanych adnotacji. Udostępnia metody pozwalające na dodawanie i usuwanie pojedynczych adnotacji oraz zdarzenia podnoszone w momencie ich dodawania i zmiany. Jest to klasa
abstrakcyjna, dziedziczy z niej klasa XmlStreamStore, która składuje adnotacje w formacie XML.
• AnnotationHelper – udostępnia kilka statycznych metod do posługiwania się adnotacjami.
Włączanie obsługi
// A stream for storing annotation.
private MemoryStream annotationStream;
// The service that manages annotations.
private AnnotationService service;
protected void window_Loaded(object sender, RoutedEventArgs e) { // Create the AnnotationService for your document container.
service = new AnnotationService(docReader);
// Create the annotation storage.
annotationStream = new MemoryStream();
AnnotationStore store = new XmlStreamStore(annotationStream);
// Enable annotations.
service.Enable(store);
}
• Zamiast MemoryStream (który zniknie po zakończeniu aplikacji) możemy użyć FileStream, co spowoduje zapisanie utworzonych adnotacji do pliku.
• Jeśli już były jakieś adnotacje w strumieniu, gdy wywołano AnnotationService.Enable(), wówczas od razu się one pojawią.
protected void window_Unloaded(object sender, RoutedEventArgs e) {
if (service != null && service.IsEnabled) {
// Flush annotations to stream.
service.Store.Flush();
// Disable annotations.
service.Disable();
annotationStream.Close();
} }
Uwaga: Każdy kontener dokumentu może mieć jedną instancję AnnotationService. Każdy dokument powinien mieć własną instancję AnnotationStore (przy otwieraniu nowego dokumentu, należy wyłączyć usługę, zamknąć strumień, stworzyć nowy Store i ponownie uruchomić usługę).
Dodawanie adnotacji (dwa sposoby)
Metody klasy AnnotationHelper:
• CreateTextStickyNoteForSelection()
• CreateInkStickyNoteForSelection()
• DeleteTextStickyNotesForSelection()
• DeleteInkStickyNotesForSelection()
• CreateHighlightsForSelection()
• ClearHighlightsForSelection()
Wszystkie działają dla aktualnego zaznaczenia.
Lub odpowiadające im polecenia klasy z AnnotationService.
<Window ... xmlns:annot=
"clr-namespace:System.Windows.Annotations;assembly=PresentationFramework"
...>
...
<ToolBar DockPanel.Dock="Top">
<Button
Command="annot:AnnotationService.CreateTextStickyNoteCommand"
>Text Note</Button>
</ToolBar>
• (Naciśnięcie przycisku spowoduje dodanie okienka, w którym użytkownik może pisać tekst adnotacji.)
• (uwaga: gdy Button jest na toolbarze, możemy pominąć CommandTarget, gdyż polecenie samo znajdzie swój cel.)
Możemy dostarczyć dodatkowych informacji dodawanej adnotacji: np. nazwę użytkownika, który ją dodał.
<Button
Command="annot:AnnotationService.CreateTextStickyNoteCommand"
CommandParameter="{StaticResource AuthorName}">
Text Note
</Button>
Z nazwą pobraną z zasobów:
<Window.Resources>
<sys:String x:Key="AuthorName">[Anonymous]</sys:String>
</Window.Resources>
Lub wczytaną z ustawień systemowych:
(System.Security.Principal.WindowsIdentity)
WindowsIdentity identity = WindowsIdentity.GetCurrent();
this.Resources["AuthorName"] = identity.Name;
Pozostałe:
<Button
Command="annot:AnnotationService.CreateInkStickyNoteCommand"
CommandParameter="{StaticResource AuthorName}">
Ink Note
</Button>
<Button
Command="annot:AnnotationService.DeleteStickyNotesCommand">
Delete Note(s)
</Button>
Dodawanie wyróżnienia wymaga podania koloru. Jest on nakładany na tekst, a zatem powinien być półprzeźroczysty (dwie pierwsze cyfry koloru, to alpha).
<Button Background="Yellow" Width="15" Height="15"
Margin="2,0"
Command="annot:AnnotationService.CreateHighlightCommand">
<Button.CommandParameter>
<SolidColorBrush Color="#54FFFF00"></SolidColorBrush>
</Button.CommandParameter>
</Button>
<Button Background="LimeGreen" Width="15" Height="15"
Margin="2,0"
Command="annot:AnnotationService.CreateHighlightCommand">
<Button.CommandParameter>
<SolidColorBrush Color="#5432CD32"></SolidColorBrush>
</Button.CommandParameter>
</Button>
I usunięcie:
<Button
Command="annot:AnnotationService.ClearHighlightsCommand">
Clear Highlights
</Button>
Uwaga: adnotacje są drukowane, zatem przed rozpoczęciem drukowania dobrze jest wyłączyć usługę adnotacji.
Przeglądanie adnotacji
Klasa AnnotationStore pozwala również przeglądać stworzone adnotacje.
IList<Annotation> annotations = service.Store.GetAnnotations();
foreach (Annotation annotation in annotations) {...
}
Własności klasy Annotation
• Id – globalny identyfikator
• AnnotationType – rodzaj adnotacji
• Anchors – wskazanie na adnotowany tekst (można też skorzystać z metody GetAnchorInfo() klasy AnnotationHelper).
• Cargos – wskazanie na dane użytkownika dołączone do adnotacji (np. tekst notki) (niskopoziomowe)
• Authors – kolekcja stringów identyfikujących autorów adnotacji
• CreationTime – data i czas utworzenia
• LastModificationTime – data i czas ostatniej modyfikacji
Własności adnotacji są tylko do odczytu, nie ma łatwego sposobu programistycznej manipulacji adnotacjami.
Reakcja na zmianę adnotacji Cztery zdarzenia:
• AnchorChanged (gdy adnotacja jest przenoszona)
• AuthorChanged (gdy zmieniana jest informacja o autorze)
• CargoChanged (gdy zmieniane są dane, np. tekst)
• StoreContentChanged (gdy jest tworzona, usuwana, etc.)
Dostosowanie wyglądu notatek Można ustawić styl:
<Style TargetType="{x:Type StickyNoteControl}">
<Setter Property="Background" Value="LightGoldenrodYellow"/>
</Style>
Więcej możliwości dadzą nam później szablony kontrolek.
Zachowanie adnotacji w Fixed Document
• W wypadku flow documents adnotacje muszą być składowane w osobnym pliku.
• W wypadku fixed documents możemy przechowywać adnotacje w pliku XPS (a nawet kilka niezależnych od siebie zbiorów adnotacji).
Uri annotationUri = PackUriHelper.CreatePartUri(
new Uri("AnnotationStream", UriKind.Relative));
Package package = PackageStore.GetPackage(doc.Uri);
PackagePart annotationPart = null;
if (package.PartExists(annotationUri))
annotationPart = package.GetPart(annotationUri);
else
annotationPart = package.CreatePart(annotationUri,
"Annotations/Stream");
AnnotationStore store =
new XmlStreamStore(annotationPart.GetStream());
service = new AnnotationService(docViewer);
service.Enable(store);