Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Surface Pen (tersedia untuk dibeli di Microsoft Store).
Gambaran Umum
Optimalkan aplikasi Windows Anda untuk penggunaan pena guna menyediakan fungsionalitas sebagai perangkat penunjuk standar dan serta memberikan pengalaman Windows Ink terbaik untuk pengguna Anda.
Nota
Topik ini berfokus pada platform Windows Ink. Untuk penanganan input pointer umum (mirip dengan mouse, sentuhan, dan touchpad), lihat Menangani input pointer.
Penggunaan tinta di aplikasi Windows Anda
Menggunakan Windows Pen dan Ink untuk membangun aplikasi perusahaan yang lebih menarik
Platform Windows Ink, bersama dengan perangkat pena, menyediakan cara alami untuk membuat catatan tulisan tangan digital, gambar, dan anotasi. Platform ini mendukung pengambilan input digitizer sebagai data tinta, menghasilkan data tinta, mengelola data tinta, merender data tinta sebagai goresan tinta pada perangkat output, dan mengonversi tinta menjadi teks melalui pengenalan tulisan tangan.
Selain menangkap posisi dasar dan pergerakan pena saat pengguna menulis atau menggambar, aplikasi Anda juga dapat melacak dan mengumpulkan berbagai jumlah tekanan yang digunakan selama stroke. Informasi ini, bersama dengan pengaturan untuk bentuk, ukuran, dan rotasi ujung pena, warna tinta, dan tujuan (tinta biasa, penghapusan, penyorotan, dan pemilihan), memungkinkan Anda untuk memberikan pengalaman pengguna yang sangat menyerupai tulisan atau gambar di kertas dengan pena, pensil, atau kuas.
Nota
Aplikasi Anda juga dapat mendukung input tinta dari perangkat berbasis pointer lainnya, termasuk digitizer sentuh dan perangkat mouse.
Platform tinta sangat fleksibel. Ini dirancang untuk mendukung berbagai tingkat fungsionalitas, tergantung pada kebutuhan Anda.
Untuk panduan Windows Ink UX, lihat kontrol tinta.
Komponen platform Windows Ink
Komponen | Deskripsi |
---|---|
InkCanvas | Pengendali platform UI XAML yang secara default menerima dan menampilkan semua input dari pena sebagai goresan tinta atau goresan penghapus. Untuk informasi selengkapnya tentang cara menggunakan InkCanvas, lihat Mengenali goresan Tinta Windows sebagai teks dan Menyimpan dan mengambil data goresan Windows Ink. |
InkPresenter | Objek code-behind, diinstansiasi bersama dengan kontrol InkCanvas (diekspose melalui properti InkCanvas.InkPresenter ). Objek ini menyediakan semua fungsionalitas penintaan default yang diekspos oleh InkCanvas, bersama dengan sekumpulan API komprehensif untuk kustomisasi dan personalisasi tambahan. Untuk informasi selengkapnya tentang cara menggunakan InkPresenter, lihat Mengenali goresan Tinta Windows sebagai teks dan Menyimpan dan mengambil data goresan Windows Ink. |
InkToolbar | Kontrol antarmuka pengguna XAML yang berisi kumpulan tombol yang dapat disesuaikan dan diperluas untuk mengaktifkan fitur terkait tinta dalam InkCanvas terkait . Untuk informasi selengkapnya tentang cara menggunakan InkToolbar, lihat Menambahkan InkToolbar ke aplikasi penintaan aplikasi Windows. |
IInkD2DRenderer | Memungkinkan penggambaran goresan tinta ke konteks perangkat Direct2D yang ditunjuk dari aplikasi Universal Windows, untuk menggantikan elemen kendali InkCanvas yang default. Ini memungkinkan penyesuaian penuh pengalaman penintaan. Untuk informasi selengkapnya, lihat Sampel tinta kompleks. |
Penintaan dasar dengan InkCanvas
Untuk menambahkan fungsionalitas penintaan dasar, cukup tempatkan kontrol platform UWP InkCanvas di halaman yang sesuai di aplikasi Anda.
Secara default, InkCanvas hanya mendukung input tinta dari pena. Input dirender sebagai garis tinta menggunakan pengaturan bawaan untuk warna dan ketebalan (pena bolpoin hitam dengan ketebalan 2 piksel), atau diperlakukan sebagai penghapus garis (ketika input berasal dari ujung penghapus atau ujung pena yang dilengkapi dengan tombol penghapus).
Nota
Jika ujung penghapus atau tombol tidak ada, InkCanvas dapat dikonfigurasi untuk memproses input dari ujung pena sebagai garis penghapus.
Dalam contoh ini, InkCanvas menutupi gambar latar belakang.
Nota
InkCanvas memiliki Tinggi
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel x:Name="HeaderPanel" Orientation="Horizontal" Grid.Row="0">
<TextBlock x:Name="Header"
Text="Basic ink sample"
Style="{ThemeResource HeaderTextBlockStyle}"
Margin="10,0,0,0" />
</StackPanel>
<Grid Grid.Row="1">
<Image Source="Assets\StoreLogo.png" />
<InkCanvas x:Name="inkCanvas" />
</Grid>
</Grid>
Rangkaian gambar ini menunjukkan bagaimana input pena dirender oleh kontrol inkCanvas ini.
![]() |
![]() |
![]() |
---|---|---|
InkCanvas kosong dengan gambar latar belakang. | InkCanvas dengan goresan tinta. | InkCanvas dengan satu garis dihapus (catat bahwa menghapus beroperasi pada seluruh garis, bukan sebagian). |
Kemampuan penintaan yang didukung oleh kontrol InkCanvas disediakan oleh objek code-behind yang disebut InkPresenter.
Untuk penintaan dasar, Anda tidak perlu khawatir dengan InkPresenter. Namun, untuk menyesuaikan dan mengonfigurasi fungsi penintaan pada InkCanvas, Anda harus mengakses objek InkPresenter yang sesuai.
Kustomisasi dasar dengan InkPresenter
Objek InkPresenter dihasilkan untuk setiap kontrol InkCanvas.
Nota
InkPresenter tidak dapat diinstansiasi secara langsung. Sebaliknya, ini diakses melalui properti InkPresenter dari InkCanvas.
Selain menyediakan semua perilaku penintaan default dari kontrol
Nota
Masukan tinta standar (dari ujung pena atau ujung/tombol penghapus) tidak dimodifikasi dengan fungsi perangkat keras sekunder, seperti tombol badan pena, tombol mouse kanan, atau mekanisme serupa.
Secara default, tinta hanya didukung untuk input pena. Di sini, kami mengonfigurasi InkPresenter untuk menginterpretasikan data input dari pena dan mouse sebagai goresan tinta. Kami juga menetapkan beberapa atribut stroke tinta awal yang digunakan untuk merender goresan ke InkCanvas.
Untuk mengaktifkan penintaan mouse dan sentuhan, atur properti InputDeviceTypes dari InkPresenter ke kombinasi nilai CoreInputDeviceTypes yang Anda inginkan.
public MainPage()
{
this.InitializeComponent();
// Set supported inking device types.
inkCanvas.InkPresenter.InputDeviceTypes =
Windows.UI.Core.CoreInputDeviceTypes.Mouse |
Windows.UI.Core.CoreInputDeviceTypes.Pen;
// Set initial ink stroke attributes.
InkDrawingAttributes drawingAttributes = new InkDrawingAttributes();
drawingAttributes.Color = Windows.UI.Colors.Black;
drawingAttributes.IgnorePressure = false;
drawingAttributes.FitToCurve = true;
inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(drawingAttributes);
}
Atribut goresan tinta dapat diatur secara dinamis untuk mengakomodasi preferensi pengguna atau persyaratan aplikasi.
Di sini, kami membiarkan pengguna memilih dari daftar warna tinta.
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel x:Name="HeaderPanel" Orientation="Horizontal" Grid.Row="0">
<TextBlock x:Name="Header"
Text="Basic ink customization sample"
VerticalAlignment="Center"
Style="{ThemeResource HeaderTextBlockStyle}"
Margin="10,0,0,0" />
<TextBlock Text="Color:"
Style="{StaticResource SubheaderTextBlockStyle}"
VerticalAlignment="Center"
Margin="50,0,10,0"/>
<ComboBox x:Name="PenColor"
VerticalAlignment="Center"
SelectedIndex="0"
SelectionChanged="OnPenColorChanged">
<ComboBoxItem Content="Black"/>
<ComboBoxItem Content="Red"/>
</ComboBox>
</StackPanel>
<Grid Grid.Row="1">
<Image Source="Assets\StoreLogo.png" />
<InkCanvas x:Name="inkCanvas" />
</Grid>
</Grid>
Kami kemudian menangani perubahan pada warna yang dipilih dan memperbarui atribut goresan tinta yang sesuai.
// Update ink stroke color for new strokes.
private void OnPenColorChanged(object sender, SelectionChangedEventArgs e)
{
if (inkCanvas != null)
{
InkDrawingAttributes drawingAttributes =
inkCanvas.InkPresenter.CopyDefaultDrawingAttributes();
string value = ((ComboBoxItem)PenColor.SelectedItem).Content.ToString();
switch (value)
{
case "Black":
drawingAttributes.Color = Windows.UI.Colors.Black;
break;
case "Red":
drawingAttributes.Color = Windows.UI.Colors.Red;
break;
default:
drawingAttributes.Color = Windows.UI.Colors.Black;
break;
};
inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(drawingAttributes);
}
}
Gambar-gambar ini menunjukkan bagaimana input pena diproses dan disesuaikan oleh InkPresenter.
InkCanvas dengan goresan tinta merah yang dipilih oleh pengguna.
Untuk menyediakan fungsionalitas di luar penintaan dan penghapusan, seperti pemilihan goresan, aplikasi Anda harus mengidentifikasi input khusus untuk inkPresenter
Input yang diteruskan untuk pemrosesan tingkat lanjut
Secara bawaan, InkPresenter memproses semua input sebagai goresan tinta atau goresan penghapusan, termasuk input yang dimodifikasi oleh perangkat keras tambahan seperti tombol barel pena, tombol mouse kanan, atau sejenisnya. Namun, pengguna biasanya mengharapkan beberapa fungsionalitas tambahan atau perilaku yang dimodifikasi dengan afordansi sekunder ini.
Dalam beberapa kasus, Anda mungkin juga perlu mengungkapkan fungsi tambahan untuk pena tanpa fitur sekunder (fungsi yang biasanya tidak terkait dengan ujung pena), jenis perangkat input lainnya, atau beberapa jenis perilaku yang dimodifikasi berdasarkan pilihan pengguna di UI aplikasi Anda.
Untuk mendukung hal ini, InkPresenter dapat dikonfigurasi untuk membiarkan input tertentu tidak diolah. Input yang tidak diproses ini kemudian diteruskan ke aplikasi Anda untuk diproses.
Contoh - Gunakan masukan mentah untuk melakukan pemilihan goresan
Platform Windows Ink tidak menyediakan dukungan bawaan untuk tindakan yang memerlukan input yang dimodifikasi, seperti pemilihan goresan. Untuk mendukung fitur seperti ini, Anda harus menyediakan solusi kustom di aplikasi Anda.
Contoh kode berikut (semua kode ada di file MainPage.xaml dan MainPage.xaml.cs) melalui cara mengaktifkan pemilihan goresan saat input dimodifikasi dengan tombol barel pena (atau tombol mouse kanan).
Pertama, kami menyiapkan UI di MainPage.xaml.
Di sini, kami menambahkan kanvas (di bawah InkCanvas) untuk menggambar goresan pemilihan. Menggunakan lapisan terpisah untuk menggambar goresan pemilihan membuat InkCanvas dan kontennya tidak tersentuh.
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel x:Name="HeaderPanel" Orientation="Horizontal" Grid.Row="0"> <TextBlock x:Name="Header" Text="Advanced ink customization sample" VerticalAlignment="Center" Style="{ThemeResource HeaderTextBlockStyle}" Margin="10,0,0,0" /> </StackPanel> <Grid Grid.Row="1"> <!-- Canvas for displaying selection UI. --> <Canvas x:Name="selectionCanvas"/> <!-- Inking area --> <InkCanvas x:Name="inkCanvas"/> </Grid> </Grid>
Dalam MainPage.xaml.cs, kami mendeklarasikan beberapa variabel global untuk menyimpan referensi ke aspek UI pemilihan. Goresan pilihan lasso dan persegi panjang pembatas yang menyoroti goresan yang dipilih.
// Stroke selection tool. private Polyline lasso; // Stroke selection area. private Rect boundingRect;
Selanjutnya, kami mengonfigurasi InkPresenter untuk menginterpretasikan data input dari pena dan mouse sebagai goresan tinta, dan menetapkan beberapa atribut goresan tinta awal yang digunakan untuk merender goresan ke dalam InkCanvas.
Yang paling penting, kami menggunakan properti InputProcessingConfigurationInkPresenter untuk menunjukkan bahwa input yang dimodifikasi harus diproses oleh aplikasi. Input yang dimodifikasi ditentukan dengan menetapkan InputProcessingConfiguration.RightDragAction nilai InkInputRightDragAction.LeaveUnprocessed. Ketika nilai ini diatur, InkPresenter meneruskan ke InkUnprocessedInput class, serangkaian peristiwa penunjuk untuk Anda tangani.
Kami menetapkan pendengar untuk PointerPressedyang tidak diproses, PointerMoved, dan pointerReleased peristiwa yang diteruskan oleh InkPresenter. Semua fungsionalitas pemilihan diimplementasikan dalam handler untuk peristiwa ini.
Terakhir, kami menetapkan listener untuk acara StrokeStarted
dan StrokesErased dari InkPresenter . Kami menggunakan handler untuk peristiwa ini untuk membersihkan UI pemilihan jika stroke baru dimulai atau stroke yang ada dihapus.public MainPage() { this.InitializeComponent(); // Set supported inking device types. inkCanvas.InkPresenter.InputDeviceTypes = Windows.UI.Core.CoreInputDeviceTypes.Mouse | Windows.UI.Core.CoreInputDeviceTypes.Pen; // Set initial ink stroke attributes. InkDrawingAttributes drawingAttributes = new InkDrawingAttributes(); drawingAttributes.Color = Windows.UI.Colors.Black; drawingAttributes.IgnorePressure = false; drawingAttributes.FitToCurve = true; inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(drawingAttributes); // By default, the InkPresenter processes input modified by // a secondary affordance (pen barrel button, right mouse // button, or similar) as ink. // To pass through modified input to the app for custom processing // on the app UI thread instead of the background ink thread, set // InputProcessingConfiguration.RightDragAction to LeaveUnprocessed. inkCanvas.InkPresenter.InputProcessingConfiguration.RightDragAction = InkInputRightDragAction.LeaveUnprocessed; // Listen for unprocessed pointer events from modified input. // The input is used to provide selection functionality. inkCanvas.InkPresenter.UnprocessedInput.PointerPressed += UnprocessedInput_PointerPressed; inkCanvas.InkPresenter.UnprocessedInput.PointerMoved += UnprocessedInput_PointerMoved; inkCanvas.InkPresenter.UnprocessedInput.PointerReleased += UnprocessedInput_PointerReleased; // Listen for new ink or erase strokes to clean up selection UI. inkCanvas.InkPresenter.StrokeInput.StrokeStarted += StrokeInput_StrokeStarted; inkCanvas.InkPresenter.StrokesErased += InkPresenter_StrokesErased; }
Kami kemudian menentukan handler untuk PointerPressedyang tidak diproses, PointerMoved, dan PointerReleased peristiwa yang diteruskan oleh InkPresenter.
Semua fungsionalitas pemilihan diimplementasikan dalam handler ini, termasuk goresan lasso dan persegi panjang pembatas.
// Handle unprocessed pointer events from modified input. // The input is used to provide selection functionality. // Selection UI is drawn on a canvas under the InkCanvas. private void UnprocessedInput_PointerPressed( InkUnprocessedInput sender, PointerEventArgs args) { // Initialize a selection lasso. lasso = new Polyline() { Stroke = new SolidColorBrush(Windows.UI.Colors.Blue), StrokeThickness = 1, StrokeDashArray = new DoubleCollection() { 5, 2 }, }; lasso.Points.Add(args.CurrentPoint.RawPosition); selectionCanvas.Children.Add(lasso); } private void UnprocessedInput_PointerMoved( InkUnprocessedInput sender, PointerEventArgs args) { // Add a point to the lasso Polyline object. lasso.Points.Add(args.CurrentPoint.RawPosition); } private void UnprocessedInput_PointerReleased( InkUnprocessedInput sender, PointerEventArgs args) { // Add the final point to the Polyline object and // select strokes within the lasso area. // Draw a bounding box on the selection canvas // around the selected ink strokes. lasso.Points.Add(args.CurrentPoint.RawPosition); boundingRect = inkCanvas.InkPresenter.StrokeContainer.SelectWithPolyLine( lasso.Points); DrawBoundingRect(); }
Untuk menyimpulkan penanganan aktivitas PointerReleased, kami membersihkan lapisan pemilihan semua konten (goresan lasso) dan kemudian menggambar persegi panjang pembatas tunggal di sekitar goresan tinta yang dicakup oleh area lasso.
// Draw a bounding rectangle, on the selection canvas, encompassing // all ink strokes within the lasso area. private void DrawBoundingRect() { // Clear all existing content from the selection canvas. selectionCanvas.Children.Clear(); // Draw a bounding rectangle only if there are ink strokes // within the lasso area. if (!((boundingRect.Width == 0) || (boundingRect.Height == 0) || boundingRect.IsEmpty)) { var rectangle = new Rectangle() { Stroke = new SolidColorBrush(Windows.UI.Colors.Blue), StrokeThickness = 1, StrokeDashArray = new DoubleCollection() { 5, 2 }, Width = boundingRect.Width, Height = boundingRect.Height }; Canvas.SetLeft(rectangle, boundingRect.X); Canvas.SetTop(rectangle, boundingRect.Y); selectionCanvas.Children.Add(rectangle); } }
Terakhir, kami mendefinisikan handler untuk peristiwa InkPresenter StrokeStarted dan StrokesErased.
Keduanya hanya memanggil fungsi pembersihan yang sama untuk menghapus pilihan saat ini setiap kali stroke baru terdeteksi.
// Handle new ink or erase strokes to clean up selection UI. private void StrokeInput_StrokeStarted( InkStrokeInput sender, Windows.UI.Core.PointerEventArgs args) { ClearSelection(); } private void InkPresenter_StrokesErased( InkPresenter sender, InkStrokesErasedEventArgs args) { ClearSelection(); }
Berikut adalah fungsi untuk menghapus semua UI pilihan dari kanvas pilihan ketika stroke baru dimulai atau stroke yang ada dihapus.
// Clean up selection UI. private void ClearSelection() { var strokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes(); foreach (var stroke in strokes) { stroke.Selected = false; } ClearDrawnBoundingRect(); } private void ClearDrawnBoundingRect() { if (selectionCanvas.Children.Any()) { selectionCanvas.Children.Clear(); boundingRect = Rect.Empty; } }
Penyajian tinta kustom
Secara bawaan, input tinta diproses pada utas latar belakang dengan latensi rendah dan dirender selama proses, atau "basah", saat digambar. Ketika goresan selesai (pena atau jari diangkat, atau tombol mouse dilepaskan), goresan diproses pada utas UI dan dirender "kering" ke lapisan InkCanvas (di atas konten aplikasi dan mengganti tinta basah).
Anda dapat mengambil alih perilaku default ini dan sepenuhnya mengontrol pengalaman penintaan dengan "pengeringan kustom" goresan tinta basah. Meskipun perilaku default biasanya cukup untuk sebagian besar aplikasi, ada beberapa kasus di mana pengeringan kustom mungkin diperlukan, ini termasuk:
- Manajemen yang lebih efisien untuk koleksi goresan tinta yang besar atau kompleks
- Dukungan untuk panning dan zooming yang lebih efisien pada kanvas tinta berukuran besar
- Menggabungkan tinta dan objek lainnya, seperti bentuk atau teks, dengan tetap mempertahankan urutan z
- Mengeringkan dan mengonversi tinta secara sinkron menjadi bentuk DirectX (misalnya, garis lurus atau bentuk yang dirasterisasi dan diintegrasikan ke dalam konten aplikasi alih-alih sebagai lapisan InkCanvas
terpisah).
Pengeringan kustom memerlukan objek IInkD2DRenderer untuk mengelola input tinta dan merendernya ke konteks perangkat Direct2D dari aplikasi Universal Windows Anda, alih-alih menggunakan kontrol InkCanvas default.
Dengan memanggil ActivateCustomDrying (sebelum InkCanvas dimuat), aplikasi membuat objek InkSynchronizer untuk menyesuaikan bagaimana goresan tinta dirender kering ke SurfaceImageSource atau VirtualSurfaceImageSource.
Baik SurfaceImageSource maupun VirtualSurfaceImageSource menyediakan permukaan bersama DirectX bagi aplikasi Anda untuk menggambar dan menyusun ke dalam konten aplikasi Anda, meskipun VSIS menyediakan permukaan virtual yang lebih besar daripada layar untuk menggeser dan memperbesar secara berkinerja. Karena pembaruan visual untuk permukaan-permukaan ini disinkronkan dengan thread UI XAML, ketika tinta dirender ke salah satu atau kedua permukaan tersebut, tinta basah dapat dihapus dari InkCanvas secara bersamaan.
Anda juga dapat mengatur penggunaan tinta kering pada SwapChainPanel, tetapi sinkronisasi dengan utas UI tidak dijamin dan mungkin ada penundaan antara saat tinta dirender ke SwapChainPanel Anda dan saat tinta dihapus dari InkCanvas.
Untuk contoh lengkap fungsionalitas ini, lihat sampel tinta Kompleks.
Nota
Pengeringan kustom dan InkToolbar
Jika aplikasi Anda mengambil alih perilaku penyajian tinta default InkPresenter dengan implementasi pengeringan kustom, goresan tinta yang dirender tidak lagi tersedia untuk InkToolbar dan perintah penghapusan bawaan InkToolbar tidak berfungsi seperti yang diharapkan. Untuk menyediakan fungsionalitas penghapusan, Anda harus mengelola semua event penunjuk, melakukan pengujian deteksi tabrakan pada setiap goresan, serta mengambil alih perintah bawaan "Hapus semua tinta".
Artikel lain di bagian ini
Topik | Deskripsi |
---|---|
Mengenali goresan tinta | Konversi goresan tinta menjadi teks menggunakan pengenalan tulisan tangan, atau ke bentuk menggunakan pengenalan kustom. |
Simpan dan ambil kembali goresan tinta | Simpan data goresan tinta dalam file Graphics Interchange Format (GIF) menggunakan metadata Embedded Ink Serialized Format (ISF). |
Menambahkan InkToolbar ke aplikasi penintaan Windows | Tambahkan InkToolbar default ke aplikasi penintaan aplikasi Windows, tambahkan tombol pena kustom ke InkToolbar, dan ikat tombol pena kustom ke definisi pena kustom. |
Artikel terkait
- Mulai: Dukung fitur tinta di aplikasi Windows Anda
- Tangani input penunjuk
- Mengidentifikasi perangkat input
- Spesifikasi Ink Serialized Format (ISF).
Antarmuka Pemrograman Aplikasi (API)
- Windows.Devices.Input
- Windows.UI.Input.Inking
- Windows.UI.Input.Inking.Core
Contoh-contoh
- Tutorial Memulai: Mendukung tinta di aplikasi Windows Anda
- Sampel tinta sederhana (C#/C++)
- Sampel tinta kompleks (C++)
- contoh tinta (JavaScript)
- Sampel buku mewarnai
- Sampel catatan keluarga
- sampel masukan dasar
- Sampel input latensi rendah
- sampel mode interaksi pengguna
- Contoh visual fokus
Arsip Sampel
Windows developer