Do wykonania ćwiczenia niezbędny jest komputer PC w postaci maszyny fizycznej lub wirtualnej z systemem Windows 7, 8, 8.1 lub 10, z zainstalowanym i skonfigurowanym środowiskiem programistycznym Microsoft Visual Studio 2019 Community Edition1).
Studenci muszą rozumieć sposób tworzenia i działania aplikacji typu “Command Line”, podstawy języka assembler oraz podstawy języka C#.
W czasie zajęć laboratoryjnych skorzystamy z modelu WPF, przy czym będziemy wykorzystywać bezpośrednie akcje (Eventy), a nie bindowanie zmiennych (Bindings) w celu ułatwienia analizy kodu. GUI będzie prostym oknem dialogowym, które będzie zawierało zestaw kontrolek służących do pobierania i wyświetlania wartości (pola typu TextBox) oraz przycisków wywołujących akcje (Button).
W ramach niniejszego laboratorium użyjemy projektu analogicznego do projektu w laboratorium nr 3, przy czym zamiast aplikacji konsolowej w języku C# zostanie wykorzystany frontend w modelu WPF (aplikacja okienkowa).
Windows Presentation Fundation (WPF) to w skrócie sposób tworzenia interfejsów użytkownika dla systemu Windows z wykorzystaniem środowiska .NET (C#, F#, VB.NET). WPF zastępuje poprzednie Windows Forms, związane z kodem niezarządzalnym i językiem C++. WPF cechuje się tym, że widok (layout, okienka i układ kontrolek) jest odseparowany od kodu zarządzającego zawartością i interakcją. Wygląd jest zapisany w postaci osobnych plików z rozszerzeniem .xaml, a kod reprezentujący okienko jest w dużej mierze generowany “w locie” podczas edycji wyglądu w edytorze Visual Studio (tzw “Code Behind”). Odseparowanie wyglądu od kodu pozwala na tworzenie modyfikacji wyglądu przez grafików, bez udziału programistów (tworzenie tzw. skórek). Dla każdego widoku (okienka) są zatem tworzone co najmniej dwa pliki: jeden z kodem, drugi z plikiem XML (.xaml) reprezentującym położenie i cechy graficzne. WPF jest skalowalny i posiada możliwość tworzenia inerfejsów użytkownika, które automatycznie dostosowują się do proporcji i rozmiarów ekranu oraz ich rozdzielczości i wymiarów (DPI). Więcej informacji można znaleźć w oficjalnym portalu Microsoft2) lub na wielu innych stronach. WPF na dzień dzisiejszy jest wiodącym środowiskiem tworzenia aplikacji okienkowych dla Windows w środowisku .NET.
W Visual Studio należy utworzyć projekt typu WPF Desktop w języku C# (rysnuek 1):
Na etapie pytania o wersję frameworku można wybrać dowolny powyżej 3.0, może to być np. 5.0 lub 4.x (o ile został zainstalowany).
Po stworzeniu projektu pojawi się okienko graficznego projektowania interfejsu użytkownika wraz ze skojarzonym plikiem .xaml. Dane w pliku opisują wygląd okienka, a zatem interakcja po stronie okienka (np. dołożenie kontrolek) powoduje automatyczną modyfikację treści pliku .xaml. Zależność ta jest prawdziwa również w drugim kierunku, czyli ręczne wpisanie tekstu do pliku .xmal spowoduje zmianę wyglądu interfejsu użytkownika. \\Główne okienko aplikacji automatycznie otrzymuje nazwę MainWindow, a zatem związany z nim plik opisujący będzie miał oznaczenie MainWindow.xaml, a kod w języku C# (tzw. Code Behind) odpowiednio MainWindow.xaml.cs.
Podczas projektowania, nowe elementy najłatwiej dodać, przeciągając je z menu [Toolbox] (na rysunku 2 po lewej stronie).
Na potrzeby naszej aplikacji wystarczą typy TextBox (nie pomylić z TextBlock!) oraz Button.
Przykładowy interface użytkownika, który będzie odpowiedni na potrzeby niniejszego projektu zaprezentowano na rysunku 2. Każdej kontrolce, do której będzie bezpośrednio odwoływać się kod (tutaj pola typu TextBox) należy nadać unikalną nazwę. Nadawanie nazw odbywa się w okienku właściwości ([Properties]) obiektu, które na rysunku 2 znajduje się w prawym dolnym rogu interfejsu użytkownika Visual Studio. Wprowadzenie nazwy ma automatycznie swoje odzwierciedlenie w pliku opisowym .xaml.
W przypadku przycisków, nie jest konieczne nadawanie im nazwy, natomiast konieczne jest utworzenie akcji związanej z kliknięciem przycisku. Akcja ta (Event) zostanie wywołana po kliknięciu danego przycisku. Funkcje przypisane do kolejnych przycisków muszą mieć unikalne nazwy, ponieważ wszystkie są umieszczone w klasie widoku okienka głównego.
Akcję można stworzyć na kilka sposobów:
.xaml oraz .xaml.cs - metoda ta nie jest polecana bo łatwo tutaj popełnić błąd.
Dla akcji wykonywanej po kliknięciu przycisku, należy wypełnić pole “Click” (rysunek 4):
btPlusOnClickfunkcja1 nie niesie ze sobą informacji ani w jakich okolicznościach ta funkcja jest wywoływana, ani dla jakiego obiektu.
Odwołanie z poziomu kodu do obiektów graficznych jest realizowane poprzez nazwę instancji obiektu graficznego. Wszystkie kontrolki typu TextBox dostarczają zawartość wpisaną w kontrolkę w postaci tekstu (String), w polu Text. W naszym przykładie interesują nas jednak dane numeryczne. W związku z tym istnieje konieczność konwersji typów z typu String to typu numerycznego (tutaj Float, analogicznie dla innych typów).
Służy do tego funkcja <typ».TryParse(…). Nietypowo, funkcja ta nie zwraca wartości bezpośrednio, lecz poprzez parametr typu out co jest wymagane jawnie podczas wywołania:
... float.TryParse(tbA1.Text, out aArg1[0]); //tbA1 - pole tekstowe, aArg1 - tablica typu float ...
Jeśli konwersja nie może zostać dokonana, ponieważ nie da się “przetłumaczyć” łańcucha na zmienną określowego typu, zostanie zgłoszony wyjątek i wykonywanie programu zostanie przerwane. Oczywiście istnieją mechanizmy walidacji, które pozwalają na wykrycie tego typu sytuacji oraz ich obsługę, niemniej jednak wykraczają one poza ramy niniejszej instrukcji.
Aby zaprezentować wynik, należy wykonać operację odwrotną: zamienić zmienną numeryczną na łańcuch znaków (String) i przypisać wartość polu Text wskazanej kontrolki. W tym celu należy użyć funkcji ToString() wywoływanej “na” wskazanej zmiennej, np. tak:
... tbC4.Text = aResults[3].ToString(); ...
W projekcie WPF należy włączyć możliwość kompilacji kodu typu unsafe oraz debbugowanie kodu natywnego, analogicznie jak to opisano w instrukcji laboratoryjnej Ćwiczenie 3: Integracja kodu niezarządzalnego, tworzonego w języku Assembler wraz z kodem zarządzalnym .NET C#.
Należy też pamiętać o wybraniu wspólnego folderu do generowania wynikowego kodu z projektu w assemblerze i projektu w języku C#, analogicznie jak to opisano w instrukcji laboratoryjnej Ćwiczenie 3: Integracja kodu niezarządzalnego, tworzonego w języku Assembler wraz z kodem zarządzalnym .NET C#. W przypadku projektów .NET, folder, do którego wykonuje się kompilację ustawia się we właściwościach projektu (prawym przyciskiem myszy należy kliknąć nagłówek projektu, a następnie wybrać “Properties” (“Właściwości”), i dalej w sekcji “Build” w grupie “Output path:” należy wskazać docelową ścieżkę (rysunek 6). Aby móc wybrać folder, musi on istnieć na dysku, zatem wskazane jest przeprowadzenie “pustej” kompilacji wyłącznie projektu DLL, aby folder ten został utworzony.
W przypadku, gdyby pomimo ustawienia wspólnego folderu wynikowego nadal były zgłaszane błędy braku biblioteki DLL, istnieje możliwość, że włączony jest system tworzenia dodatkowych podfolderów, jak to opisano poniżej w notce z uwagami. Aby usunąć ten problem, konieczne jest ręczna edycja pliku konfiguracyjnego projektu.
Aby otworzyć plik konfiguracyjny projektu, należy dwukrotnie klinąć nazwe projektu w drzewie. Otworzy się plik typu XML, z rozszerzeniem .csproj.
<PropertyGroup> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> </PropertyGroup>
Do solucji należy dodać projekt w assemblerze. Kod w assemblerze należy skompilować do biblioteki DLL, a następnie dołączyć ją dynamicznie do rozwiązania w C#. Jest to dokładnie ten sam proces, który został opisany w Ćwiczenie 3: Integracja kodu niezarządzalnego, tworzonego w języku Assembler wraz z kodem zarządzalnym .NET C# w zakresie tworzenia i konfiguracji części assemblerowej oraz jej udostępniania po stronie C#, natomiast zamiast aplikacji konsolowej występuje aplikacja WPF.
Należy utworzyć prosty kalkulator, który przeliczy wektorowo dla 4 zmiennych wyniki dla operacji dodawania, odejmowania, mnożenia i dzielenia (akcje na osobnych przyciskach). Wymagane jest przeliczenie więcej niż jednego zestawu danych w ramach jednej instrukcji dodawania (trzeba skorzystać z SIMD). Zadanie należy wykonać na typach zmiennoprzecinkowych, pojedynczej precyzji (32 bity, po stronie C# typ float).
Należy zadeklarować po jednej funkcji assemblerowej dla każdej operacji.
Wartości najwygodniej będzie przekazać do funkcji assemblerowych wpisując je po stronie kodu w C# do dwóch tablic źródłowych, a na wyniki zaalokować tablicę trzecią. Liczba zmiennych jest znana (4) i stała, zatem długości tablic nie trzeba przekazywać do funkcji assemblerowej. Wskaźniki na tablice można przekazać poprzez rejestry lub poprzez wskaźnik na strkturę zawierającą wskaźniki do ww. tablic (do wyboru).
Tablice po stronie C# można zadeklarować np następująco:
... public partial class MainWindow : Window { public float[] aArg1 = { 0, 0, 0, 0 }; //argument 1 public float[] aArg2 = { 0, 0, 0, 0 }; //argument 2 public float[] aResults = { 0, 0, 0, 0 }; //wynik ...
partial w deklaracji typu klasy MainWindow oznacza, że w pliku zaimplementowana jest jedynie część klasy. Pozostała część kodu znajduje się w pliku zarządzanym automatycznie przez edytor GUI okienka. Dla dewelopera w praktyce ten mechanizm jest przezroczysty, a cały kod należy umieszczać w tym pliku (.xaml.cs).
MOVAPS, tylko jej wolniejszą wersję, nie wymagającą specyficznego wyrównania zmiennych w pamięci, czyli MOVUPS.
Sprawdzenie debuggerem wraz z podglądem rejestrów / stanu pamięci oraz obserwacja wyników i sprawdzenie obliczeń.
Visual Studio 2019: https://visualstudio.microsoft.com/vs/