المحتوى عن 'xamarin-forms'.



مزيد من الخيارات

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المُحتوى


التصنيفات

  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • نصائح وإرشادات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • التجارة الإلكترونية
  • الإدارة والقيادة
  • مقالات ريادة أعمال عامة

التصنيفات

  • PHP
    • Laravel
    • ووردبريس
  • جافاسكريبت
    • Node.js
    • jQuery
    • AngularJS
    • Cordova
  • HTML5
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • سي شارب #C
    • منصة Xamarin
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • برمجة أندرويد
  • لغة Swift
  • لغة R
  • لغة TypeScript
  • سير العمل
    • Git
  • صناعة الألعاب
    • Unity3D
  • مقالات برمجة عامة

التصنيفات

  • تجربة المستخدم
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
    • كوريل درو
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • مقالات تصميم عامة

التصنيفات

  • خواديم
    • الويب HTTP
    • قواعد البيانات
    • البريد الإلكتروني
    • DNS
    • Samba
  • الحوسبة السّحابية
    • Docker
  • إدارة الإعدادات والنّشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • مقالات DevOps عامة

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات

التصنيفات

  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • مقالات عمل حر عامة

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
  • أندرويد
  • iOS
  • macOS
  • ويندوز

التصنيفات

  • شهادات سيسكو
    • CCNA
  • شهادات مايكروسوفت
  • شهادات Amazon Web Services
  • شهادات ريدهات
    • RHCSA
  • شهادات CompTIA
  • مقالات عامة

أسئلة وأجوبة

  • الأقسام
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة البرمجة
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات
    • أسئلة الشهادات المتخصصة

التصنيفات

  • ريادة الأعمال
  • العمل الحر
  • التسويق والمبيعات
  • البرمجة
  • التصميم
  • DevOps

تمّ العثور على 10 نتائج

  1. سنتناول في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms الجزء الثاني من تطبيق قارئ الخلاصات الخاص بموقع أكاديميّة حسوب. لقد بنينا في الجزء الأوّل تطبيق أساسي أسميناه BasicFeedReader وكانت وظيفته تنحصر في عرض خلاصات قسم مقالات البرمجة. سنطوّر هذه الفكرة من خلال إنشاء تطبيق جديد يعمل على إتاحة الإمكانية للمستخدم بأن يستعرض الخلاصات المتاحة من جميع أقسام الموقع. سيكون ذلك من خلال إضافة صفحة جديدة لعرض أقسام الموقع على شكل مجموعات. لنبدأ فورًا في بناء هذا التطبيق التي سيحتوي على بعض من المكوّنات المتطابقة مع سلفه. بناء تطبيق قارئ الخلاصات المحسّن أنشئ مشروعًا جديدًا من النوع Blank App (Xamarin.Forms Portable) وسمّه EnhancedFeedReader ثم أبق فقط على المشروعين EnhancedFeedReader (Portable) و EnhancedFeedReader.Droid كما وسبق أن فعلنا في هذا الدرس. رغم الشبه الكبير بين هذا التطبيق وسلفه إلًّا أنّني آثرت إعادة إدراج الشيفرة البرمجيّة للأجزاء المتشابهة، وذلك لكي يكون هذا التطبيق متكاملًا قائمًا بحد ذاته. أضف المجلّدين التاليين إلى المشروع EnhancedFeedReader (Portable) (عن طريق النقر بزر الفأرة الأيمن ثم اختيار Add ثم New Folder): Pages و Entities. الأصناف ضمن المجلّد Entities أضف الأصناف التالي إلى المجلّد Entities: FeedItem و Section و GroupSection. بالنسبة للصنف FeedItem فلقد تحدثنا عنه في الجزء الأوّل، وإليك الشيفرة البرمجيّة الخاصّة به: namespace EnhancedFeedReader.Entities { public class FeedItem { public string Link { get; set; } public string Title { get; set; } public string Description { get; set; } public override string ToString() { return Title; } } } الصنف Section هو صنف جديد تمامًا وهو يمثّل قسم من أقسام المحتوى الخاصّة بموقع أكاديميّة حسّوب. الشيفرة البرمجيّة للملف Section.cs هي: using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; namespace EnhancedFeedReader.Entities { public class Section { public string Name { get; set; } public string ResourceUrl { get; set; } public async Task<ObservableCollection<FeedItem>> GetFeedItems() { return await Task.Factory.StartNew(() => { XDocument doc = XDocument.Load(ResourceUrl); var feeds = from newsItem in doc.Descendants("item") select new FeedItem { Title = newsItem.Element("title").Value, Description = newsItem.Element("description").Value, Link = newsItem.Element("link").Value }; return new ObservableCollection<FeedItem>(feeds); }); } public override string ToString() { return Name; } } } يحتوي هذا الصنف على خاصيّتين: الاسم Name وعنوان المصدر ResourceUrl الذي سيتم جلب الخلاصات منه. يوجد أيضًا التابع GetFeedItems الذي سيجلب الخلاصات التابعة للقسم الحالي. هذا التابع هو البديل للتابع LoadFeeds الذي كان موجودًا في الصنف SectionFeedsPage وهو يقوم بنفس وظيفته مع ميزة استخدام البرمجة غير المتزامنة. وأخيرًا التابع ToString للحصول على تمثيل نصّي لكائن من النوع Section. أمّا بالنسبة للصنف GroupSection فهو يمثّل تصنيف لمجموعة من الأقسام. في الحقيقة يعمل موقع أكاديميّة حسوب على تقسيم الخلاصات ضمن مجموعات كل منها يحتوي على قسمين. انظر إلى الشيفرة الخاصّ بهذا الصنف: using System.Collections.ObjectModel; namespace EnhancedFeedReader.Entities { public class GroupSection : ObservableCollection<Section> { public string Name { get; set; } } } لاحظ كم هي بسيطة هذه الشيفرة. يحتوي هذا الصنف على خاصيّة واحدة هي خاصيّة الاسم Name له. مع الانتباه إلى أنّه يرث من الصنف العمومي ObservableCollection<Section>. وهذا إشارة إلى أنّه يمثّل مجموعة من الأقسام Section. سنرى سبب عمليّة الوراثة هذه بعد قليل. الواجهات ضمن المجلّد Pages يوجد لدينا ضمن هذا المجلّد ثلاث واجهات تتبع لثلاثة أصناف. صنفان منهما قديمان وهما: SectionFeedsPage و FeedDetailsPage وهما يمثّلان على الترتيب: الواجهة المسؤولة عن عرض خلاصات قسم محدّد من الأقسام المتوفّرة، ومحتوى الخلاصة التي اختارها المستخدم من الواجهة السابقة. أمّا الصنف الثالث فهو SectionsPage وهو يمثّل الواجهة الرئيسيّة الخاصة بعرض جميع الأقسام المتوفّرة ضمن موقع الأكاديميّة. انتقل إلى ملف الرماز SectionFeedsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="EnhancedFeedReader.Pages.SectionFeedsPage"> <StackLayout> <ListView x:Name="lsvFeeds" ItemTapped="lsvFeeds_ItemTapped"/> </StackLayout> </ContentPage> ثم انتقل إلى ملف الشيفرة الموافق له وهو SectionFeedsPage.xaml.cs واحرص على أن تكون محتوياته على الشكل التالي: using EnhancedFeedReader.Entities; using Xamarin.Forms; namespace EnhancedFeedReader.Pages { public partial class SectionFeedsPage : ContentPage { public SectionFeedsPage(Section section) { InitializeComponent(); Title = section.Name; PopulateFeedsListView(section); } private async void PopulateFeedsListView(Section section) { lsvFeeds.ItemsSource = await section.GetFeedItems(); } private async void lsvFeeds_ItemTapped(object sender, ItemTappedEventArgs e) { FeedItem selectedFeed = (FeedItem)e.Item; FeedDetailsPage feedDetailsPage = new FeedDetailsPage(selectedFeed); await Navigation.PushAsync(feedDetailsPage); } } } الجديد في هذا الصنف هو التابع PopulateFeedsListView الذي يُمرّر إليه وسيط واحد من النوع Section حيث يتم الحصول على خلاصاته ومن ثمّ إسنادها إلى القائمة lsvFeeds لعرضها. لاحظ كيف يتمّ ذلك باستخدام البرمجة غير المتزامنة. انتقل بعد ذلك إلى ملف الرماز FeedDetailsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="EnhancedFeedReader.Pages.FeedDetailsPage"> <StackLayout Orientation="Vertical"> <WebView x:Name="wvDescription" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"/> </StackLayout> </ContentPage> ثم انتقل إلى ملف الشيفرة البرمجيّة الموافق له وهو FeedDetailsPage.xaml.cs: using Xamarin.Forms; using EnhancedFeedReader.Entities; namespace EnhancedFeedReader.Pages { public partial class FeedDetailsPage : ContentPage { public FeedDetailsPage(FeedItem feedItem) { InitializeComponent(); this.Title = feedItem.Title; var descriptionHtmlSource = new HtmlWebViewSource(); descriptionHtmlSource.Html = @"<html dir='rtl'><body>" + feedItem.Description + "</body></html>"; wvDescription.Source = descriptionHtmlSource; } } } لم يطرأ في الحقيقة أيّ تعديل على هذا الصنف. انتقل أخيرًا إلى ملف الرماز SectionsPage.xaml الذي يمثّل واجهة العرض الأساسيّة، التي ستعرض جميع أقسام الخلاصات المتاحة في الأكاديميّة واحرص على أن تكون محتوياته كما يلي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="EnhancedFeedReader.Pages.SectionsPage"> <ListView x:Name="lsvSections" ItemTapped="lsvSections_ItemTapped" IsGroupingEnabled="True" GroupDisplayBinding="{Binding Name}" /> </ContentPage> واضح أنّ محتوياته بسيطة، فهي لا تتعدى عنصر القائمة الذي أسميته lsvSections. الأمر الملفت للنظر هنا هو وجود الخاصيتين IsGroupingEnabled و GroupDisplayBinding لعنصر القائمة. أسندنا القيمة True للخاصيّة IsGroupingEnabled وذلك للإشارة إلى وجوب أن تدعم هذه القائمة ميزة التجميع Grouping. أمّا الخاصيّة GroupDisplayBinding فقد أسندت لها القيمة {Binding Name} للإشارة إلى أنّني أرغب بأن يتم ربطها مع قيمة الخاصيّة Name للكائن GroupSection فهي تُحدّد النص المراد إظهاره كعنوان لكل مجموعة. لكي تتوضّح الأمور بشكل أفضل انظر إلى الواجهة في حالة العمل: انتقل الآن إلى ملف الشيفرة البرمجيّة الموافق وهو SectionsPage.xaml.cs لكي نتعرّف على طريقة الاستفادة من هذه الميزة، واحرص على أن تكون محتوياته على الشكل التالي: using System.Collections.Generic; using System.Collections.ObjectModel; using Xamarin.Forms; using EnhancedFeedReader.Entities; namespace EnhancedFeedReader.Pages { public partial class SectionsPage : ContentPage { public SectionsPage() { InitializeComponent(); Title = "قارئ خلاصات أكاديمية حسوب"; LoadGroupSections(); } private void LoadGroupSections() { int spacePos = 0; string tmp; GroupSection gs = null; Dictionary<string, string[]> groupSectionDic = new Dictionary<string, string[]>() { { "خلاصات ريادة الأعمال",new string[] { "https://academy.hsoub.com/entrepreneurship/?rss=1", "https://academy.hsoub.com/questions/rss/entrepreneurship-5.xml" } }, { "خلاصات العمل الحر",new string[] { "https://academy.hsoub.com/freelance/?rss=1", "https://academy.hsoub.com/questions/rss/freelance-8.xml" } }, { "خلاصات التسويق والمبيعات", new string[] { "https://academy.hsoub.com/marketing/?rss=1", "https://academy.hsoub.com/questions/rss/marketing-7.xml" } }, { "خلاصات البرمجة", new string[] { "https://academy.hsoub.com/programming/?rss=1", "https://academy.hsoub.com/questions/rss/programming-3.xml" } }, { "خلاصات التصميم", new string[] { "https://academy.hsoub.com/design/?rss=1", "https://academy.hsoub.com/questions/rss/design-4.xml" } }, { "خلاصات DevOps",new string[] { "https://academy.hsoub.com/devops/?rss=1", "https://academy.hsoub.com/questions/rss/devops-6.xml" } }, { "خلاصات البرامج والتطبيقات",new string[] { "https://academy.hsoub.com/apps/?rss=1", "https://academy.hsoub.com/questions/rss/apps-9.xml" } }, { "خلاصات الشهادات المتخصصة",new string[] { "https://academy.hsoub.com/certificates/?rss=1", "https://academy.hsoub.com/questions/rss/certificates-10.xml" } } }; ObservableCollection<GroupSection> groupSections = new ObservableCollection<GroupSection>(); foreach (var grp in groupSectionDic) { gs = new GroupSection() { Name = grp.Key }; spacePos = grp.Key.IndexOf(" "); tmp = grp.Key.Substring(spacePos + 1); gs.Add( new Section { Name = "مقالات " + tmp , ResourceUrl = grp.Value[0] } ); gs.Add( new Section { Name = "أسئلة " + tmp, ResourceUrl = grp.Value[1] } ); groupSections.Add(gs); } lsvSections.ItemsSource = groupSections; } private async void lsvSections_ItemTapped(object sender, ItemTappedEventArgs e) { Section selectedSection = (Section)e.Item; SectionFeedsPage sectionFeedsPage = new SectionFeedsPage(selectedSection); await Navigation.PushAsync(sectionFeedsPage); } } } هناك الكثير من المتعة في الشيفرة السابقة! تتمحور معظم الشيفرة حول التابع LoadGroupSections ويُستَخدم لتجهيز الأقسام المختلفة لمصادر الخلاصات في الأكاديمية ووضعها ضمن مجموعات منفصلة ضمن عنصر القائمة lsvSections. هذه الأقسام (كعناوين) موجودة في وضع عدم اتصال وقد نسختها يدويًا من موقع الأكاديميّة، وهي ضمن المتغير groupSectionDic الذي صرحنا عنه من نوع القاموس Dictionary ملف التطبيق App.cs انتقل إلى ملف التطبيق App.cs واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new NavigationPage(new SectionsPage()); } احرص على استخدام فضاء الاسم Pages لكي تستطيع الوصول إلى الصفحة SectionsPage كما يلي: using EnhancedFeedReader.Pages; ستضع السطر السابق أوّل الملف App.cs كما هو معلوم. نفّذ البرنامج وتنقّل في أقسامه المختلفة. الخلاصة تناولنا في هذا الدرس الجزء الثاني من تطبيق قارئ الخلاصات الخاص بموقع أكاديميّة حسّوب. لقد أجرينا تحسينات مهمّة على هذا التطبيق من خلال السماح للمستخدم بتصفّح الخلاصات من جميع أقسام الموقع. حيث استخدمنا ميزة التجميع grouping لعنصر القائمة ListView وذلك لتجميع العناوين التابعة لنفس القسم معًا. نكون بذلك قد انتهينا من هذا التطبيق.
  2. هذا الدرس هو الجزء الثالث والأخير من سلسلة دروس تُعنى بكيفيّة بناء تطبيق عملي بسيط لإدارة جهات اتصال ببيانات أوليّة وهو بطبيعة الحال جزء من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms. ستحتاج إلى قراءة الجزأين الأوّل والثاني السابقين لكي تستطيع المتابعة في هذا الدرس. سنهتم في هذا الدرس ببناء واجهتي التطبيق: الواجهة الرئيسيّة التي يمكن من خلالها البحث عن جهات الاتصال حسب الاسم والكنية، وأيضًا إضافة جهة اتصال جديدة. الواجهة الخاصة بعرض تفاصيل جهة الاتصال التي تمّ اختيارها من الواجهة الرئيسيّة، ومن ثمّ إمكانيّة تعديل بياناتها أو حتى حذفها. بالإضافة إلى تعديل الصنف App لكي يصبح التطبيق قابل للعمل. الواجهة الرئيسيّة للتطبيق ContactsPage سنحتاج في البداية إلى إضافة مجلّد جديد سنسمّه Pages سنضع فيه أي صفحة جديدة للتطبيق، وهذا الإجراء هو من باب تنظيم مكوّنات التطبيق فحسب. من نافذة مستكشف الحل Solution Explorer انقر بزر الفأرة الأيمن على اسم المشروع ContactsApp (Portal) ثم اختر من القائمة التي ستظهر الخيار Add. من القائمة الفرعية، اختر New Folder ثمّ سمّه Pages. انقر بزر الفأرة الأيمن على المجلّد الذي أضفته توًا وهو Pages واختر من القائمة Add ثم اختر من القائمة الفرعية New Item لتظهر نافذة تسمح باختيار العنصر الجديد الذي تودّ إضافته. اختر صفحة محتوى تعتمد على الرماز كما وأن سبق لنا أن فعلنا ذلك في هذا الدرس. سمّ هذه الصفحة ContactsPage. انتقل الآن إلى الملف ContactsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="ContactsApp.Pages.ContactsPage" Appearing ="ContactsPage_Appearing"> <StackLayout> <StackLayout Padding="5,25,5,5"> <StackLayout Orientation="Horizontal"> <Label Text="First Name" /> <Editor x:Name="txtFirstName" HorizontalOptions="FillAndExpand" TextChanged="Editor_TextChanged"/> </StackLayout> <StackLayout Orientation="Horizontal"> <Label Text="Last Name" /> <Editor x:Name="txtLastName" HorizontalOptions="FillAndExpand" TextChanged="Editor_TextChanged"/> </StackLayout> <Button Text="Find" x:Name="btnFind" Clicked="btnFind_Clicked"/> </StackLayout> <StackLayout> <ListView x:Name="lsvContacts" ItemTapped="lsvContacts_ItemTapped"/> </StackLayout> <Button Text="+ New Contact" HorizontalOptions="FillAndExpand" Clicked="btnNewContact_Clicked"/> </StackLayout> </ContentPage> لقد استخدمنا أربعة أحداث مختلفة في هذه الصفحة سنورد وصفًا قصيرًا لكل منها فيما يلي: حدث الظهور Appearing للصفحة ContactsPage وقد ربطناه بالمعالج ContactsPage_Appearing وذلك لتحديث بيانات الصفحة عند العودة من الصفحة المسؤولة عن عرض تفاصيل جهة الاتصال كما سنرى لاحقًا في هذا الدرس. حدث تغيّر النص TextChangedلكلّ من العنصرين txtFirstName و txtLastName وقد ربطناه بالمعالج Editor_TextChanged وذلك لمسح نتائج البحث التي تظهر ضمن القائمة lsvContacts عند أي تعديل يجريه المستخدم فيهما. حدث لمس المُدخل ضمن القائمة lsvContacts وربطناه بالمعالج lsvContacts_ItemTapped وذلك لكي ينتقل البرنامج إلى الواجهة المسؤولة عن عرض تفاصيل جهة الاتصال عند لمسها. حدث النقر Clicked لزر إضافة جهة اتصال جديدة وربطناه بالمعالج btnNewContact_Clicked. انتقل بعد ذلك إلى ملف الشيفرة البرمجيّة الموافق للملف السابق وهو ContactsPage.xaml.cs واحرص على أن تكون محتوياته على الشكل التالي: using System; using Xamarin.Forms; using ContactsApp.Entities; namespace ContactsApp.Pages { public partial class ContactsPage : ContentPage { public ContactsPage() { InitializeComponent(); } private async void DoFind() { string firstName = txtFirstName.Text == null ? "" : txtFirstName.Text; string lastName = txtLastName.Text == null ? "" : txtLastName.Text; lsvContacts.ItemsSource = await((App)Application.Current).ContactsRepository .GetContactsAsync(firstName, lastName); } private void btnFind_Clicked(object sender, EventArgs e) { DoFind(); } private void Editor_TextChanged(object sender, TextChangedEventArgs e) { if (lsvContacts.ItemsSource != null) lsvContacts.ItemsSource = null; } private async void lsvContacts_ItemTapped(object sender, ItemTappedEventArgs e) { Contact selectedContact = (Contact)e.Item; ContactDetailsPage ContactDetailsPage = new ContactDetailsPage(selectedContact); await Navigation.PushAsync(ContactDetailsPage); } private void ContactsPage_Appearing(object sender, EventArgs e) { DoFind(); } private async void btnNewContact_Clicked(object sender, EventArgs e) { ContactDetailsPage ContactDetailsPage = new ContactDetailsPage(null); await Navigation.PushAsync(ContactDetailsPage); } } } يحتوي هذا الملف على الصنف ContactsPage والذي يحتوي على معالجات الأحداث التي صرّحنا عنها في صفحة الرماز الموافقة له كما رأينا قبل قليل، بالإضافة إلى وجود تابع خدمي وهو DoFind ووظيفته إجراء عمليّة بحث وفقًا للمعايير التي يرغبها المستخدم (الاسم والكنية). الشيفرة البرمجيّة الموجودة ضمن معالجات الأحداث بسيطة وواضحة. ولكن أريد أن أتوقّف قليلًا عند الشيفرة البرمجيّة التي يستخدمها التابع الخدمي DoFind التي تنفّذ عمليّة البحث: string firstName = txtFirstName.Text == null ? "" : txtFirstName.Text; string lastName = txtLastName.Text == null ? "" : txtLastName.Text; lsvContacts.ItemsSource = await((App)Application.Current).ContactsRepository .GetContactsAsync(firstName, lastName); أوّل سطرين واضحان حيث يعملان على الحصول على معايير البحث التي يريدها المستخدم من مربّعي النص txtFirstName و txtLastName. أمّا السطر الأخير فهو يعمل على الاتصال بالمستودع لتنفيذ عمليّة البحث وإرجاع النتائج وذلك بإجراء استدعاء إلى التابع GetContactsAsync وتمرير معياري البحث إليه. انظر هذه العبارة: await((App)Application.Current).ContactsRepository .GetContactsAsync(firstName, lastName); تعمل الخاصية Application.Current على إرجاع كائن من النوع Application يمثّل التطبيق الحالي الذي نعمل من خلاله. والذي يحتاج بدوره إلى عملية تحويل cast باستخدام التحويل (App) إلى كائن من النوع App وهو الصنف الأساسي في التطبيق. في الحقيقة لقد صرّحت عن الخاصيّة ContactsRepository في الصنف App ليتم الوصول للمستودع من أيّ مكان من تطبيقنا. سنرى التصريح عن هذه الخاصيّة بعد قليل. واجهة تفاصيل جهة الاتصال ContactDetailsPage أضف هذه الواجهة بنفس الأسلوب الذي اتبعناه عند إضافة ملف الواجهة الرئيسيّة، أي إلى المجلّد Pages. على أن يكون اسمها ContactDetailsPage. سيصبح مستكشف الحل لديك شبيه بما يلي: انتقل إلى ملف الرماز ContactDetailsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="ContactsApp.Pages.ContactDetailsPage"> <StackLayout Orientation="Vertical" Padding="5,25,5,5"> <Label Text="FirstName" /> <Entry x:Name ="txtFirstName"/> <Label Text="LastName" /> <Entry x:Name ="txtLastName"/> <Label Text="Tel" /> <Entry x:Name ="txtTel"/> <Label Text="EMail" /> <Entry x:Name ="txtEMail"/> <Label Text="Hobbies" /> <Editor x:Name ="txtHobbies"/> <StackLayout Orientation="Horizontal"> <Button Text="Save" x:Name="btnSave" Clicked="btnSave_Clicked"/> <Button Text="Delete" x:Name="btnDelete" Clicked="btnDelete_Clicked"/> </StackLayout> </StackLayout> </ContentPage> واضح أنّه ملف بسيط. هناك حدثان قد صرّحنا عنهما في هذه الصفحة وهما: حدث النقر لزر الحفظ Clicked وقد ربطناه بالمعالج btnSave_Clicked وهو مسؤول عن عمليتي الحفظ والإضافة. حدث النقر لزر الحذف Clicked وقد ربطناه بالمعالج btnDelete_Clicked وهو مسؤول عن حذف جهة الاتصال. انتقل إلى ملف الشيفرة البرمجيّة الموافق لملف الرماز السابق واسمه ContactDetailsPage.xaml.cs واحرص على أن تكون محتوياته على الشكل التالي: using System; using Xamarin.Forms; using ContactsApp.Entities; namespace ContactsApp.Pages { public partial class ContactDetailsPage : ContentPage { private Contact currentContact; public ContactDetailsPage(Contact contact) { InitializeComponent(); this.currentContact = contact; if (this.currentContact != null) { txtFirstName.Text = contact.FirstName; txtLastName.Text = contact.LastName; txtEMail.Text = contact.EMail; txtTel.Text = contact.Tel; txtHobbies.Text = contact.Hobbies; Title = contact.ToString() + " Details"; btnDelete.IsVisible = true; } else { Title = "New Contact"; btnDelete.IsVisible = false; } } private async void btnSave_Clicked(object sender, EventArgs e) { string firstName = txtFirstName.Text == null ? "" : txtFirstName.Text; string lastName = txtLastName.Text == null ? "" : txtLastName.Text; string tel = txtTel.Text == null ? "" : txtTel.Text; string email = txtEMail.Text == null ? "" : txtEMail.Text; string hobbies = txtHobbies.Text == null ? "" : txtHobbies.Text; if (this.currentContact != null) { this.currentContact.FirstName = firstName; this.currentContact.LastName = lastName; this.currentContact.EMail = tel; this.currentContact.Tel = email; this.currentContact.Hobbies = hobbies; await ((App)Application.Current).ContactsRepository .UpdateContactAsync(this.currentContact); } else { Contact contact = new Contact { FirstName = firstName, LastName = lastName, Tel = tel, EMail = email, Hobbies = hobbies, }; await ((App)Application.Current).ContactsRepository .AddContactAsync(contact); } await Navigation.PopAsync(); } private async void btnDelete_Clicked(object sender, EventArgs e) { var result = await DisplayAlert("Delete Confirmation", "Are you sure you want to delete this contact?", "Yes", "No"); if (result) { await ((App)Application.Current).ContactsRepository .DeleteContactAsync(this.currentContact); await Navigation.PopAsync(); } } } } كما وأوضحنا مسبقًا أنّ وظيفة هذه الواجهة هي عرض تفاصيل جهة اتصال وتعديلها، مع إمكانيّة حذفها، بالإضافة إلى استخدام هذه الواجهة في إضافة جهة اتصال جديدة أيضًا. تميّز هذه الواجهة الغرض المطلوب منها عن طريق كائن من النوع Contact يُمرّر كوسيط إلى بانيتها. إذا كان هذا الوسيط يحتوي على كائن صالح من النوع Contact فهذا يعني أنّنا نريد عرض جهة الاتصال التي يمثّلها هذا الكائن، ومن ثمّ تعديلها أو حذفها، ويقتضي ذلك بالطبع إظهار زر الحذف Delete. أمّا في حال تمّ تمرير null كوسيط إلى البانية، فهذا يعني أنّه تمّ استدعاء الصفحة لإنشاء جهة اتصال جديدة تمامًا، وبالتالي إخفاء زر الحذف Delete لأنّه لن يكون له معنى في هذه الحالة. يتم حفظ نسخة من الكائن الممرّر ضمن حقل خاص ضمن الصنف اسمه currentContact. ويتم تحديد الغاية من الواجهة ضمن البانية نفسها. انظر إلى تعريف معالج الحدث btnSave_Clicked من الشيفرة السابقة وانظر كيف يتعامل مع الحالتين السابقتين (تعديل أو إضافة). الصنف App انتقل إلى الملف App.cs واحرص على أن تكون محتوياته على الشكل التالي: using Xamarin.Forms; using ContactsApp.Abstract; using ContactsApp.Concrete; using ContactsApp.Pages; namespace ContactsApp { public class App : Application { public IContactsRepository ContactsRepository { get; set; } public App() { ContactsRepository = new MemoryContactsRepository(); // The root page of your application MainPage = new NavigationPage(new ContactsPage()); } protected override void OnStart() { // Handle when your app starts } protected override void OnSleep() { // Handle when your app sleeps } protected override void OnResume() { // Handle when your app resumes } } } لاحظ بدايةّ الخاصيّة ContactsRepository من النوع IContactsRepository الموجود ضمن تعريف الصنف App: public IContactsRepository ContactsRepository { get; set; } في الحقيقة ستمثّل هذه الخاصيّة المستودع الذي سنتبادل البيانات من خلاله. سنعمل على إنشاء كائن جديد من النوع MemoryContactsRepository ومن ثمّ نسنده إلى هذه الخاصيّة وذلك ضمن بانية الصنف App كما يلي: ContactsRepository = new MemoryContactsRepository(); وهذا الأمر جائز تمامًا لأنّ الصنف MemoryContactsRepositroy يحقّق الواجهة IContactsRepository فهو بمثابة وارث منها. بما أنّ هذه الخاصيّة عامّة public فسيكون بإمكان جميع أجزاء التطبيق الوصول للكائن ContactsRepository وبالتالي التعامل مع البيانات من خلال مكان واحد. لاحظ أيضًا من بانية الصنف App كيف أنشأنا كائن جديد من الصنف NavigationPage وأسندناه إلى الخاصيّة MainPage للصنف App. وذلك لأنّنا نريد أن يدعم تطبيقنا ميزة التنقّل بين الصفحات التي تحدثنا عنها في هذا الدرس. يمكنك الآن تنفيذ التطبيق وتجربة جميع المزايا التي يتمتّع بها. الخلاصة نكون بهذا الدرس قد أنهينا بناء تطبيق جهات الاتصال، حيث تحدثنا عن كيفيّة بناء واجهتي التطبيق، وكيفيّة التنقّل بين هاتين الواجهتين، وكيف تتصّلان بالمستودع عند الحاجة للتعامل مع مصدر البيانات الذي يكون في تطبيقنا هذا عبارة عن مجموعة تتكوّن من عناصر من النوع Contact موجودة في ذاكرة التطبيق.
  3. سنتابع في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms العمل الذي بدأناه في الدرس السابق والمتمثّل ببناء تطبيق جهات الاتصال. قد أنهينا في الدرس السابق بناء نموذج المستودع من خلال التصريح عن الواجهة IContactsRepository وتحقيقها من خلال الصنف MemoryContactsRepository. كما أنشأنا الصنف Contacts الذي يمثّل حجر البناء الأساسي في التطبيق. سنضيف في هذا الدرس النواحي الوظيفيّة للصنف المستودع MemoryContactsRepository لكي يصبح تطبيقنا قابلًا للعمل. تجهيز النواحي الوظيفيّة للمستودع افتح الملف MemoryContactsRepository.cs واحرص على أن تكون محتوياته على الشكل التالي: using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Collections.ObjectModel; using ContactsApp.Abstract; using ContactsApp.Entities; namespace ContactsApp.Concrete { public class MemoryContactsRepository : IContactsRepository { private ObservableCollection<Contact> contacts; public MemoryContactsRepository() { contacts = new ObservableCollection<Contact>() { new Contact() { Id=1, FirstName = "Ahmad", LastName="Saeed", Tel="123456", EMail="admin@example.com", Hobbies="Swimming" }, new Contact() { Id=2, FirstName = "Mahmood", LastName="Maktabi", Tel="852136", EMail="info@example.com", Hobbies="Reading" }, new Contact() { Id=3, FirstName = "Mazen", LastName="Najem", Tel="987456", EMail="it@example.com", Hobbies="Swimming" }, new Contact() { Id=4, FirstName = "Sawsan", LastName="Hilal", Tel="741258", EMail="sales@example.com", Hobbies="Writing, Reading" }, new Contact() { Id=5, FirstName = "Musab", LastName="Aga", Tel="357159", EMail="admin@example.com", Hobbies="Sport" } }; } public async Task<ObservableCollection<Contact>> GetContactsAsync(string firstName, string lastName) { return await Task.Factory.StartNew(() => { IEnumerable<Contact> result = from contact in contacts where contact.FirstName .ToUpper() .Contains(firstName.ToUpper()) && contact.LastName .ToUpper() .Contains(lastName.ToUpper()) select contact; ObservableCollection<Contact> tmp = new ObservableCollection<Contact>(result); return tmp; }); } public async Task<bool> AddContactAsync(Contact contactToAdd) { return await Task.Factory.StartNew(() => { contactToAdd.Id = contacts.Count() + 1; contacts.Add(contactToAdd); return true; }); } public async Task<bool> UpdateContactAsync(Contact contactToUpdate) { return await Task.Factory.StartNew(() => { Contact result = (from contact in contacts where contact.Id == contactToUpdate.Id select contact).FirstOrDefault(); if (result != null) { contactToUpdate.FirstName = result.FirstName; contactToUpdate.LastName = result.LastName; contactToUpdate.EMail = result.EMail; contactToUpdate.Tel = result.Tel; contactToUpdate.Hobbies = result.Hobbies; return true; } else { return false; } }); } public async Task<bool> DeleteContactAsync(Contact contactToDelete) { return await Task.Factory.StartNew(() => { var result = (from contact in contacts where contact.Id != contactToDelete.Id select contact); if (result != null) { contacts = new ObservableCollection<Contact>(result); contactToDelete = null; return true; } else { return false; } }); } } } الجديد هنا أنّنا قد أسندنا شيفرة برمجيّة لكل من التوابع الأساسيّة الموجودة في المستودع. انظر إلى الفقرات التالية التي تشرح عمل الشيفرة البرمجيّة ضمن كل تابع. تابع البحث GetContactsAsync فيما يلي التابع GetContactsAsync والذي يتطلّب وسيطين من النوع string للبحث حسب الاسم والكنية لجهات الاتصال: public async Task<ObservableCollection<Contact>> GetContactsAsync(string firstName, string lastName) { return await Task.Factory.StartNew(() => { IEnumerable<Contact> result = from contact in contacts where contact.FirstName .ToUpper() .Contains(firstName.ToUpper()) && contact.LastName .ToUpper() .Contains(lastName.ToUpper()) select contact; ObservableCollection<Contact> tmp = new ObservableCollection<Contact>(result); return tmp; }); } واضح أنّ هذه الشيفرة تقوم بإنشاء مهمة جديدة (تابع على شكل تعبير lambda) عن طريق التابع Task.Factory.StartNew (ستحتاج إلى إنعاش ذاكرتك بهذا الدرس) وذلك باستخدام تقنية البرمجة غير المتزامنة لمنع جمود التطبيق كما نعلم. المتغيّر result الذي تراه في الشيفرة السابق هو من النوع IEnumerable<Contact> أي مجموعة قابلة للعد عناصرها من النوع Contact، وهو يحصل على هذه المجموعة من خلال استعلام LINQ To Objects بسيط: from contact in contacts where contact.FirstName .ToUpper() .Contains(firstName.ToUpper()) && contact.LastName .ToUpper() .Contains(lastName.ToUpper()) select contact; تعمل هذه الشيفرة ببساطة على الاستعلام عن جميع جهات الاتصال الموجودة ضمن المتغير contacts والتي تحتوي على عبارتي البحث الخاصّتين بالاسم والكنية بنفس الوقت. إذا كان لديك خبرة باستعلامات SQL فستجد LINQ to Objects مألوفة. لاحظ وجود عبارتي return ضمن هذا التابع GetContactsAsync. العبارة التي تأتي أولًا هي العبارة المسؤولة عن إرجاع كائن من النوع Task<ObservableCollection<Contact>> أمّا العبارة الثانيّة التي تأتي في الأسفل فهي المسؤولة عن إرجاع كائن من النوع Task<ObservableCollection<Contact>> من التابع الداخلي وهو عبارة عن تعبير lambda والذي يتم تنفيذه من خلال التابع Task.Factory.StartNew كما هو واضح. في آخر سطرين من التابع الداخلي (تعبير lambda) نعمل على تقديم نتيجة البحث على شكل مجموعة عموميّة وهي ObservableCollection<Contact> حيث يتم تحويلها ضمنيًّا إلى كائن من النوع Task<ObservableCollection<Contact>>. تابع إضافة جهة اتصال جديد AddContactAsync التابع AddContactAsync يتطلّب وسيطًا واحدًا فقط من النوع Contact ويُرجع كائن من النوع Task<bool> للإشارة إلى نجاح عمليّة الإضافة: public async Task<bool> AddContactAsync(Contact contactToAdd) { return await Task.Factory.StartNew(() => { contactToAdd.Id = contacts.Count() + 1; contacts.Add(contactToAdd); return true; }); } تعمل هذه الشيفرة على إضافة جهة اتصال جديدة إلى جهات الاتصال الموجودة مسبقًا ضمن المتغيّر contacts مع الانتباه إلى هذه العبارة البرمجيّة: contactToAdd.Id = contacts.Count() + 1; ما تفعله هذه العبارة غير موجود في أيّ تطبيق عمليّ حقيقي. فهي تُسند قيمة للخاصيّة Id لجهة الاتصال الممرّرة إلى التابع. لا ينبغي لأي تطبيق أن يقوم بهذا العمل، فهذا الرقم ينبغي أن يتم توليده تلقائيًّا عند إضافة جهة الاتصال إلى قاعدة البيانات. وبما أنّ تطبيقنا يعتمد على بيانات وهمية موجودة في الذاكرة فكان لزامًا علينا القيام بمثل هذا الأمر لتمييز جهات الاتصال المضافة حديثًا. تابع الحذف DeleteContactAsync التابع DeleteContactAsync يتطلّب وسيطًا واحدًا فقط من النوع Contact ويُرجع كائن من النوع Task<bool> للإشارة إلى نجاح عمليّة الحذف: public async Task<bool> DeleteContactAsync(Contact contactToDelete) { return await Task.Factory.StartNew(() => { var result = (from contact in contacts where contact.Id != contactToDelete.Id select contact); if (result != null) { contacts = new ObservableCollection<Contact>(result); contactToDelete = null; return true; } else { return false; } }); } تعمل الشيفرة السابقة في الواقع على فلترة جهة الاتصال المراد حذفها مما يعني ببساطة أنّ استعلام LINQ to Objects سيعمل على إرجاع جميع جهات الاتصال باستثناء تلك التي يكون قيمة الخاصية Id لها مطابقة لقيمة الخاصيّة Id لجهة الاتصال التي نرغب بحذفها. ومن ثمّ يتم إسناد النتيجة إلى المتغيّر contacts مما يعني فعليًّا أنّنا قد حذفنا جهة الاتصال هذه من الذاكرة. تابع التحديث UpdateContactAsync التابع UpdateContactAsync والذي يتطلّب وسيطًا واحدًا فقط من النوع Contact ويُرجع كائن من النوع Task<bool> للإشارة إلى نجاح عمليّة الحذف: public async Task<bool> UpdateContactAsync(Contact contactToUpdate) { return await Task.Factory.StartNew(() => { Contact result = (from contact in contacts where contact.Id == contactToUpdate.Id select contact).FirstOrDefault(); if (result != null) { contactToUpdate.FirstName = result.FirstName; contactToUpdate.LastName = result.LastName; contactToUpdate.EMail = result.EMail; contactToUpdate.Tel = result.Tel; contactToUpdate.Hobbies = result.Hobbies; return true; } else { return false; } }); } تخضع هذه الشيفرة البرمجيّة لنفس المبدأ: يبحث استعلام LINQ to Objects عن جهة الاتصال مطلوبة ضمن جهات الاتصال الموجودة ضمن المتغيّر contacts حسب قيمة الخاصيّة Id لجهة الاتصال، وبعد الحصول عليها، يتم تحديث قيم الخصائص الأخرى كما هو واضح. بهذه الطريقة أصبح نموذج المستودع جاهزًا لوضعه في الاستخدام. حيث ستتعامل معه جميع مكوّنات التطبيق لإنجاز أربعة أنواع مختلفة من العمليّات على البيانات: البحث، والإضافة، والتعديل، والحذف. الخلاصة تناولنا في هذا الدرس كيفيّة بناء نموذج المستودع الذي يمثّله الصنف MemoryContactsRepository والذي يحقّق كما نعلم الواجهة IContactsRepository حيث تعرّفنا على التوابع الأربعة ضمنه التي تسمح لمكوّنات التطبيق بالتعامل مع البيانات دون الاهتمام بمكان وجود هذه البيانات. اطلعنا أيضًا على كيفيّة توظيف تقنيّة الاستعلام LINQ to Objects ضمن التوابع الأربعة لتنفيذ المهام المطلوبة من هذا المستودع. سنبدأ في الدرس التالي ببناء واجهات التطبيق. حقوق الصورة البارزة محفوظة لـ Freepik
  4. سنهتمّ في هذا الدرس من سلسلة تعلّم تطوير تطبيقات أندرويد باستخدام Xamarin.Forms بالعناصر المرئيّة الشائعة في Xamarin.Forms. يحتاج أيّ تطبيق بصورة عامة إلى العديد من العناصر المرئيّة التي تظهر على الشاشة بحيث توفّر معلومات معيّنة للمستخدم أو تسمح له بالتفاعل معها. يرث أيّ عنصر مرئيّ في Xamarin.Forms من الصنف VisualElement، سنتحدّث في هذا الدرس عن العنصر Entry وهو عبارة عن حقل خاص بالإدخال يستقبل مُدخلات المستخدم. وأيضًا عن العنصر ListView وهو عنصر القائمة ووظيفته عرض العناصر على شكل قائمة قابلة للتمرير. كما سنتحدّث عن العنصر Switch وهو عنصر تحويل الحالة المنطقيّة ويحمل إحدى قيمتين true أو false. وأخيرًا سنتحدّث عن عنصر المنزلق Slider ووظيفته السماح للمستخدم باختيار قيمة ضمن مجال محدّد عن طريق السحب. سنتناول تطبيقين بسيطين لفهم كيفيّة التعامل مع هذه العناصر. تطبيق إدخال النصوص فكرة هذا التطبيق بسيطة، حيث سنتعلّم كيف سنستخدم العنصر Entry للسماح للمستخدم بإدخال قيم نصيّة، ستُضاف هذه القيم إلى عنصر القائمة ListView. أنشئ مشروعًا من النوع Blank App (Xamarin.Forms Portable) وسمّه TextEntriesApp، ثم أبق فقط على المشروعين TextEntriesApp (Portable) و TextEntriesApp.Droid كما وسبق أن فعلنا في هذا الدرس. بعد ذلك أضف صفحة محتوى جديدة كما فعلنا في هذا الدرس وسمّها TextEntriesPage. احرص على أن تكون محتويات الملف TextEntriesPage.cs على الشكل التالي: 1 using System.Collections.Generic; 2 using Xamarin.Forms; 3 4 namespace TextEntriesApp 5 { 6 public class TextEntriesPage : ContentPage 7 { 8 public TextEntriesPage() 9 { 10 List<string> entries = new List<string>(); 11 12 Entry txtInput = new Entry 13 { 14 HorizontalOptions = LayoutOptions.FillAndExpand, 15 HorizontalTextAlignment = TextAlignment.End, 16 Placeholder = "أدخل كلمة", 17 FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)) 18 }; 19 20 Button btnAddEntry = new Button 21 { 22 Text = "+", 23 HorizontalOptions = LayoutOptions.Start 24 }; 25 btnAddEntry.Clicked += (s, e) => 26 { 27 entries.Add(txtInput.Text); 28 29 txtInput.Text = ""; 30 txtInput.Focus(); 31 }; 32 33 StackLayout inputAreaSLayout = new StackLayout 34 { 35 Orientation = StackOrientation.Horizontal, 36 VerticalOptions = LayoutOptions.Start, 37 Children = 38 { 39 btnAddEntry, 40 txtInput 41 } 42 }; 43 44 ListView lstEntries = new ListView 45 { 46 ItemsSource = entries, 47 VerticalOptions = LayoutOptions.FillAndExpand 48 }; 49 50 51 Content = new StackLayout 52 { 53 Children = 54 { 55 inputAreaSLayout, 56 lstEntries 57 } 58 }; 59 } 60 } 61 } انتقل إلى الملف App.cs واحرص على أن تكون بانيته على الشكل التالي: public App() { // The root page of your application MainPage = new TextEntriesPage(); } نفّذ البرنامج باستخدام F5 لتحصل على شكل شبيه بما يلي: البرنامج السابق بسيط، اكتب كلمة ثم انقر الزر (+). تابع على هذا المنوال حتى تختفي الكلمات أسفل الشاشة، ثم اسحب بإصبعك على هذه القائمة لتظهر الكلمات المختفية. بدأنا في السطر 10 بالتصريح عن المتغيّر entries من نوع List<string> وأسندنا إليه كائنًا جديدًا. يحتوي هذا المتغيّر على المُدخلات التي يدخلها المستخدم، وذلك عند النقر على الزر (+) عند كل إضافة. سنسند هذا المتغيّر إلى الخاصيّة ItemSource لكائن القائمة ListView كما سنرى بعد قليل. أنشأنا في الأسطر من 12 حتى 18 كائن Entry جديد وأسندناه إلى المتغيّر txtInput. لقد أسندنا القيمة TextAlignment.End للخاصية HorizontalTextAlignment لكي يظهر النص محاذًا نحو اليمين (باعتبار أنّنا نستخدم اللغة العربية في إدخال النصوص)، كما تجدر ملاحظة الخاصيّة الجديدة Placeholder التي تسمح بإظهار علامة مائيّة ضمن كائن Entry لإرشاد المستخدم حول وظيفة حقل الإدخال هذا. توجد أيضًا خاصيّة أخرى اسمها PlaceholderColor للتحكّم بلون خط هذه العلامة المائيّة. يمكن الوصول إلى محتوى حقل الإدخال عن طريق الخاصيّة Text له. قمنا بعد ذلك في الأسطر من 20 حتى 31 بإضافة زر جديد وأسندناه إلى المتغيّر btnAddEntry وجهزّنا معالج حدث النقر الخاص به. بالنسبة لمخطّط المكدّس الذي أنشأناه وأسندناه إلى المتغيّر inputAreaSLayout في الأسطر من 33 حتى 42، فتنحصر وظيفته في جعل كلّ من حقل الإدخال txtInput وزر الإضافة btnAdd يظهران بشكل أفقي متجاور. نأتي الآن إلى أكثر العناصر أهميّةً ألا وهو عنصر القائمة ListView الذي أنشأنا كائنًا جديدًا منه وأسندناه إلى المتغيّر lstEntries في الأسطر من 44 حتى 48. يُستخدَم عنصر القائمة بكثرة في تطبيقات الأجهزة المحمولة، إذ أنّ طبيعته التي تساعد على تمرير محتوياته تجعل منه مناسبًا جدًّا للاستخدام على الشاشات الصغيرة. في الواقع لم نستخدمه في هذا التطبيق إلًّا بأبسط صوره، حيث سنفرد له درسين كاملين للحديث عنه مستقبلًا. يمتلك عنصر القائمة الخاصيّة ItemSource حيث أسندنا لها المتغيّر entries ليقوم بعرض محتوياته ضمنه. وفي كلّ مرّة سنضيف فيها عنصرًا جديدًا على entries سينعكس ذلك على محتويات القائمة أيضًا. ضبطنا أيضًا الخاصيّة VerticalOptions للقائمة بحيث تشغل الحيّز المتبقّي ضمن الصفحة. تطبيق التحكّم بالخط تعتمد فكرة هذا التطبيق على وجود عنصر Switch بالإضافة إلى عنصر Slider وأخيرًا لصيقة تحتوي على نصٍّ بسيط نريد التحكّم بحجمه (عن طريق العنصر Slider) والتحكّم بميول الخط (عن طريق العنصر Switch). أنشئ مشروعًا من النوع Blank App (Xamarin.Forms Portable) وسمّه FontControlApp، ثم أبق فقط على المشروعين FontControlApp (Portable) و FontControlApp.Droid. بعد ذلك أضف صفحة محتوى جديدة وسمّها FontControlPage. احرص على أن تكون محتويات الملف FontControlPage.cs على الشكل التالي: 1 using Xamarin.Forms; 2 3 namespace FontControlApp 4 { 5 public class FontControlPage : ContentPage 6 { 7 private Label lblText; 8 public FontControlPage() 9 { 10 Switch swtItalicFont = new Switch 11 { 12 HorizontalOptions = LayoutOptions.EndAndExpand 13 }; 14 swtItalicFont.Toggled += (s, e) => 15 { 16 if (e.Value) 17 lblText.FontAttributes = FontAttributes.Italic; 18 else 19 lblText.FontAttributes = FontAttributes.None; 20 }; 21 22 StackLayout fontItalicSLayout = new StackLayout 23 { 24 VerticalOptions = LayoutOptions.Start, 25 Orientation = StackOrientation.Horizontal, 26 Children = 27 { 28 swtItalicFont, 29 new Label 30 { 31 Text = "خط مائل", 32 HorizontalOptions = LayoutOptions.End 33 } 34 } 35 }; 36 37 Slider sldFontSize = new Slider 38 { 39 VerticalOptions = LayoutOptions.Center, 40 HorizontalOptions = LayoutOptions.FillAndExpand, 41 Maximum = 40, 42 Minimum = 10, 43 Value = 20 44 }; 45 sldFontSize.ValueChanged += (s, e) => 46 { 47 lblText.FontSize = e.NewValue; 48 }; 49 50 lblText = new Label 51 { 52 VerticalOptions = LayoutOptions.FillAndExpand, 53 VerticalTextAlignment = TextAlignment.Center, 54 HorizontalTextAlignment = TextAlignment.Center, 55 Text="This is a sample text to demonstrate Switch and Slider elements", 56 TextColor = Color.Accent, 57 FontFamily = "Tahoma", 58 FontSize = 20 59 }; 60 61 Content = new StackLayout 62 { 63 Children = 64 { 65 fontItalicSLayout, 66 sldFontSize, 67 lblText 68 } 69 }; 70 71 Padding = new Thickness(5); 72 } 73 } 74 } ثم انتقل إلى الملف App.cs واحرص على أن تكون بانيته على الشكل التالي: public App() { // The root page of your application MainPage = new FontControlPage(); } نفّذ البرنامج باستخدام F5، وتعامل معه قليلًا لتحصل على شكل شبيه بما يلي: صرّحنا في الأسطر من 10 حتى 20 عن كائن Switch جديد وأسندناه إلى المتغيّر swtItalicFont، كما قمنا بإسناد معالج للحدث Toggle كما هو واضح (السطر 14). يتم تفعيل الحدث Toggle عندما ينقر المستخدم على عنصر Switch ليغيّر حالته المنطقيّة من true إلى false أو بالعكس. من الممكن معرفة الوضع الحالي لعنصر Switch بقراءة الخاصيّة Value من الكائن المرّر كوسيط e كما فعلنا في السطر 16. ومن ثمّ نتخذ القرار المناسب في جعل الخط مائلًا أم لا من خلال استخدام المعدودة FontAttributes كما فعلنا في السطرين 17 و19. أنشأنا في الأسطر من 22 حتى 35 مخطّط مكدّس جديد تنحصر وظيفته في جعل عنصر Switch بالإضافة إلى لصيقة صغيرة توضّح عمله بشكل متجاور أفقيًّا وعلى الجهة اليمنى كما هو واضح. بالنسبة لعنصر Slider فقد صرّحنا عنه في الأسطر من 37 حتى 44. حيث أنشأنا كائن جديد من الصنف Slider وأسندناه إلى المتغيّر sldFontSize. تُعتبر عناصر Slider من العناصر المفيدة في واجهة المستخدم، وتُستخدم عادةً لاختيار قيم محصورة ضمن مجال يمكن تحديده مسبقًا. طريقة اختيار هذه القيم هي التي تجعل منها عناصر مميّزة. فأنت تحتاج فقط إلى لمس الدائرة الصغيرة وسحبها إلى اليمين أو اليسار لكي تختار القيمة التي تناسبك. يتم تحديد مجال الاختيار عن طريق الخاصيتين Minimum وMaximum. احرص دومًا على تعيين قيمة الخاصية Maximum قبل Minimum. يمكن قراءة القيمة الحالية ضمن عنصر Slider عن طريق الخاصيّة Value له. لقد عملنا في الشيفرة السابقة على ضبط قيمة افتراضيّة لعنصر Slider وهي 15 (انظر السطر 43) كما جعلنا مجال الاختيار يتراوح بين 10 و40 (السطران 41 و42). الأمر المفيد الآخر في هذا العنصر هو الحدث ValueChanged الذي يُفعّل كلّما تغيّرت القيمة الحالية لعنصر Slider. لقد أسندنا معالج حدث في الأسطر 45 حتى 48 للاستفادة من هذا الحدث، حيث نستفيد من قيمة عنصر Slider مباشرةً في تغيير حجم الخط للنص الموجود ضمن اللصيقة lblText المصرّح عنها في الأسطر من 50 حتى 59. لقد عيّنّا نوع الخط المستخدم في هذه اللصيقة عن طريقة ضبط القيمة "Tahoma" للخاصيّة FontFamily لها (السطر 57). لقد اخترت الخط Tahoma لكي يظهر الخط المائل بشكل واضح. توجد ملاحظة بسيطة أخيرة في الشيفرة السابقة، وهي استخدام شكل جديد لبانية الصنف Thickness (السطر 71) حيث مرّرنا إليها وسيطًا واحدًا فقط بدلًا من أربعة وسائط كما كنّا نفعل من قبل. في الحقيقة عندما تريد تمرير نفس القيمة لكلّ من الجهات الأربع (top، left، right، bottom) فيكفيك أن تمرّر قيمة واحدة فقط تمثّل القيمة الثابتة لهذه الاتجاهات. الخلاصة تحدثنا في هذا الدرس عن بعض العناصر المرئيّة المفيدة في Xamarin.Forms. حيث تناولنا عنصر حقل الإدخال Entry وعنصر تحويل الحالة المنطقية Switch وعنصر المنزلق Slider لاختيار قيمة ضمن مجال محدّد يمكن تعيينه. والعنصر المهم ListView وهو عنصر القائمة. سنتعامل مع هذه العناصر مجّددًا في هذه السلسلة، وسنتوسّع بالحديث عن بعض منها في دروس مخصّصة. سنتناول في الدرس التالي المزيد من هذه العناصر مع تطبيقات ممتعة لها.
  5. سنتابع عملنا في سلسلة تعلّم تطوير تطبيقات أندرويد باستخدام Xamarin.Forms مع الجزء الثاني للعناصر المرئيّة الشائعة في Xamarin.Forms. تناولنا في الجزء الأوّل بعضًا من هذه العناصر حيث تحدثنا عن عناصر القائمة ListView وحقل الإدخال Entry وعنصر تحويل الحالة المنطقيّة Switch كما تحدثنا أيضًا عن عنصر المنزلق Slider. سنتحدّث في هذا الدرس عن عنصر DatePicker الذي يسمح باختيار تاريخ محدّد بطريقة جميلة، كما سنتحدّث عن عنصر الاختيار الخطوي Stepper الذي يشبه عنصر المنزلق Slider، وأيضًا عنصر الإطار Frame الذي يُستَخدم لتعزيز الناحية الجمالية لواجهة المستخدم. سنتناول تطبيقين عمليّين لتوضيح عمل هذه العناصر. تطبيق التحكّم بعرض الحدود وهو تطبيق بسيط للغاية، الغرض منه توضيح كيفيّة استخدام العنصر Stepper. يمتلك هذا العنصر بنية برمجيّة شبيهة بعنصر المنزلق Slider الذي تحدثنا عنه في الدرس السابق، رغم أنّه يختلف عنه من الناحية الشكليّة، فهذا العنصر يتكوّن من زرّين متجاورين يظهر عليهما النصّان "+" و "-". توجد ميزة إضافية في هذا العنصر تتمثّل في وجود خاصيّة اسمها Increment قيمتها الافتراضيّة 1. وتُستخدم لتحديد مقدار الزيادة أو النقصان عند كل نقرة على الزرّين السابقين كما سنرى بعد قليل. يُستخدم عنصر Stepper غالبًا لاختيار قيم عدديّة صحيحة (بدون فاصلة عشريّة) مع أنّه من الممكن استخدام القيم العشرية معه. أنشئ مشروعًا من النوع Blank App (Xamarin.Forms Portable) وسمّه StepperDemoApp، ثم أبق فقط على المشروعين StepperDemoApp (Portable) و StepperDemoApp.Droid كما وسبق أن فعلنا في هذا الدرس. بعد ذلك أضف صفحة محتوى جديدة كما فعلنا في هذا الدرس وسمّها StepperDemoPage. احرص على أن تكون محتويات الملف StepperDemoPage.cs على الشكل التالي: 1 using Xamarin.Forms; 2 3 namespace StepperDemoApp 4 { 5 public class StepperDemoPage : ContentPage 6 { 7 public StepperDemoPage() 8 { 9 Button btnDemo = new Button 10 { 11 Text = "اختبار سماكة حدود الزر", 12 FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Button)), 13 BorderColor = Color.FromHex("C0C0C0"), 14 BackgroundColor = Color.FromHex("404040"), 15 HorizontalOptions = LayoutOptions.Center, 16 VerticalOptions = LayoutOptions.CenterAndExpand 17 }; 18 19 Label lblCurrentBorderWidth = new Label 20 { 21 Text = "السماكة الحالية: 0", 22 HorizontalOptions = LayoutOptions.FillAndExpand, 23 HorizontalTextAlignment = TextAlignment.Center 24 }; 25 26 Stepper stepper = new Stepper 27 { 28 HorizontalOptions = LayoutOptions.Center, 29 Maximum = 10, 30 Minimum = 0 31 }; 32 stepper.ValueChanged += (s, e) => 33 { 34 btnDemo.BorderWidth = stepper.Value; 35 36 lblCurrentBorderWidth.Text = "السماكة الحالية: " + stepper.Value.ToString(); 37 }; 38 39 StackLayout changeBorderWidthSLayout = new StackLayout 40 { 41 VerticalOptions = LayoutOptions.CenterAndExpand, 42 Children = 43 { 44 lblCurrentBorderWidth, 45 stepper 46 } 47 }; 48 49 Content = new StackLayout 50 { 51 Children = 52 { 53 btnDemo, 54 changeBorderWidthSLayout 55 } 56 }; 57 } 58 } 59 } انتقل إلى الملف App.cs واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new StepperDemoPage(); } نفّذ البرنامج باستخدام F5، ثم تفاعل مع التطبيق بنقر الزرّين "+" و "-" لتحصل على شكل شبيه بما يلي: يتكوّن التطبيق من زر في الأعلى يحوي النص "اختبار سماكة الحدود" بالإضافة إلى عنصر Stepper يسمح لك بضبط قيمة الحدود لهذا الزر، وأيضًا توجد لصيقة صغيرة تعلو العنصر Stepper وظيفتها عرض القيمة الحالية المختارة لسماكة حد الزر. صرّحنا في الأسطر من 9 حتى 17 عن عنصر زر جديد وأسندناه إلى المتغيّر btnDemo. لقد جعلنا الخط المُستخدَم في هذا الزر كبيرًا، وأيضًا ضبطنا لون الحد عن طريق الخاصيّة BorderColor (السطر 13) ليكون هذا اللون "C0C0C0" وهي قيمة ست عشرية تتكوّن من 3 مكوّنات لونية هي من اليسار إلى اليمين: الأحمر والأخضر والأزرق بحيث يأخذ كل مكوّن لوني محرفين. فمن خلال القيمة "C0C0C0" سيأخذ كل من الأحمر والأخضر والأزرق القيمة C0 وهي حالة خاصّة بالطبع وسنحصل من خلالها على أحد تدرّجات اللون الرمادي. بنفس الأسلوب قمنا بضبط لون الخلفيّة لهذا الزر من خلال الخاصيّة BackgroundColor (السطر 14) ليكون "404040" وهو أيضًا أحد تدرّجات الرمادي. يمكنك اختيار أيّ لون ترغبه. سنضع كلًّا من اللصيقة المسؤولة عن عرض قيمة الحد الحالي lblCurrentBorderWidth (الأسطر من 19 حتى 24) وأيضًا عنصر الاختيار الخطوي stepper (الأسطر من 26 حتى 31) ضمن مخطّط مكدّس ليظهرا متجاورين رأسيًّا. مخطّط المكدّس هذا مصرّح عنه في الأسطر من 39 حتى 47 عن طريق المتغيّر changeBorderWidthSLayout. لاحظ أخيرًا أنّنا قد صرّحنا عن معالج الحدث ValueChanged للعنصر stepper بصورة مماثلة تمامًا لما فعلناه مع عنصر المنزلق Slider في الدرس السابق، وذلك في الأسطر من 32 حتى 37 لكي نعالج نقرات المستخدم على هذا العنصر، وبنفس الوقت نحدّث قيمة اللصيقة لتعرض السماكة الحاليّة لحدود الزر (السطر 36). تطبيق حساب العمر بالأيّام نحتاج لإنجاز هذا التطبيق إلى عنصر DatePicker لاختيار تاريخ الولادة، وعنصر Frame لإكساب صفة جمالية على التطبيق كما سنرى، كما سنحتاج إلى زر لإجراء عمليّة الحساب ولصيقة لعرض النتيجة. سيتم حساب العمر الحالي بالأيّام من تاريخ الولادة حتى تاريخ اليوم. أنشئ مشروعًا من النوع Blank App (Xamarin.Forms Portable) وسمّه AgeCalculatorApp، ثم أبق فقط على المشروعين AgeCalculatorApp (Portable) وAgeCalculatorApp.Droid. بعد ذلك أضف صفحة محتوى جديدة وسمّها AgeCalculatorPage. احرص على أن تكون محتويات الملف AgeCalculatorPage.cs على الشكل التالي: 1 using System; 2 using Xamarin.Forms; 3 4 namespace AgeCalculatorApp 5 { 6 public class AgeCalculatorPage : ContentPage 7 { 8 public AgeCalculatorPage() 9 { 10 Frame frmWelcome = new Frame 11 { 12 VerticalOptions = LayoutOptions.Start, 13 Padding = new Thickness(15), 14 BackgroundColor = Color.FromRgba(Color.Accent.R, 15 Color.Accent.G, 16 Color.Accent.B, 17 0.2), 18 OutlineColor = Color.Accent, 19 Content = new Label 20 { 21 Text = "تطبيق حساب العمر بالأيّام", 22 FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)), 23 TextColor = Color.Aqua, 24 HorizontalTextAlignment = TextAlignment.Center 25 } 26 }; 27 28 29 DatePicker birthDate = new DatePicker 30 { 31 VerticalOptions = LayoutOptions.Start, 32 HorizontalOptions = LayoutOptions.FillAndExpand, 33 34 }; 35 36 Label lblCurrentDate = new Label 37 { 38 Text = DateTime.Now.ToString("dd/MM/yyyy"), 39 VerticalOptions = LayoutOptions.Start, 40 HorizontalOptions = LayoutOptions.FillAndExpand, 41 FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)) 42 }; 43 44 Label lblResult = new Label 45 { 46 VerticalOptions = LayoutOptions.FillAndExpand, 47 FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), 48 VerticalTextAlignment = TextAlignment.Start, 49 HorizontalTextAlignment = TextAlignment.Center, 50 TextColor = Color.Accent 51 }; 52 53 Button btnCalculate = new Button 54 { 55 Text = "احسب", 56 VerticalOptions = LayoutOptions.Start 57 }; 58 btnCalculate.Clicked += (s, e) => 59 { 60 TimeSpan diff = DateTime.Now - birthDate.Date; 61 62 lblResult.Text = string.Format("عمرك {0} يومًا", diff.Days); 63 }; 64 65 Content = new StackLayout 66 { 67 Children = 68 { 69 frmWelcome, 70 birthDate, 71 lblCurrentDate, 72 btnCalculate, 73 lblResult 74 } 75 }; 76 77 Padding = new Thickness(5); 78 } 79 } 80 } انتقل بعد ذلك إلى الملف App.cs وتأكّد أنّ بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new AgeCalculatorPage(); } نفّذ البرنامج باستخدام F5 لتحصل على شكل شبيه بما يلي: اختر تاريخ مولدك (الميلادي) ولاحظ الصندوق الذي يملأ حيزًا لا بأس به من الشاشة، وكف يسمح لك باختيار التاريخ بصورة جميلة. بعد أن تختار التاريخ المناسب لك، انقر زر "احسب" لتحصل على عمرك بالأيّام من الأسفل. أنشأنا في الأسطر من 10 حتى 26 كائن جديد من الصنف Frame وأسندناه إلى المتغيّر frmWelcome. تُعتبر عناصر Frame ذات بنية مستطيلة تُستخدم لأغراض تحسين واجهة المستخدم. يعرض عنصر Frame حدًّا مستطيلًا يحيط بمحتوى معيّن. وإليك الآن بعض الملاحظات البسيطة حوله: يحتوي عنصر الإطار Frame على الخاصيّة Content التي يمكن أن نُسند إليها أي عنصر آخر، أو من الممكن أن نسند إليها مخطّط مكدّس يحتوي بدوره على العديد من العناصر الأخرى. في مثالنا هذا أسندنا إلى هذه الخاصيّة كائن لصيقة Label (السطر 19) يحتوي على عنوان التطبيق. يضم عنصر الإطار أيضًا الخاصيّة Padding لإضافة حشوة داخليّة بين حدود الإطار والمحتوى الذي يقع ضمنه (السطر 13). يمكن التحكّم بلون حد الإطار من خلال الخاصيّة OutlineColor (السطر 18) حيث عملنا في هذا التطبيق على إسناد اللون Color.Accent إليه. الخاصيّة BackgroundColor للإطار تتحكّم بلون الخلفيّة كما نعلم، ولكن لاحظ الشكل الجديد الذي استخدمناه في إسناد اللون لها هذه المرّة (الأسطر من 14 حتى 17). استخدمنا هذه المرّة التابع Color.FromRgba الذي يُرجع كائن من النوع Color اعتبارًا من القيم اللونية RGB ومقدار الشفافيّة A الممرّرة إليه. ونستفيد من هذا الشكل في الحصول على ألوان مخصّصة أكثر. أحببت أن أستخدم اللون Accent ولكن بشكل شفّاف لذلك حصلت على المركّبات اللونية له باستخدام Color.Accent.R للأحمر وColor.Accent.G للأخضر وColor.Accent.B للأزرق، ومرّرتها إلى التابع Color.FromRgba بنفس الترتيب السابق. أمّا الوسيط الأخير فقد مرّرت إليه القيمة 0.2 ليظهر اللون شفَّافًا. قيمة الوسيط الأخير (قيمة A) تتراوح بين 0 (شفّاف تمامًا) و1 (معتم تمامًا). بالنسبة للعنصر DatePicker فيُستخدم كما أشرنا لاختيار تاريخ محدّد بصورة جميلة. صرّحنا عنه في الأسطر من 29 حتى 34 وأسندنا الكائن الناتج إلى المتغيّر birthDate ويُعبّر عن تاريخ الولادة. إليك بعض الملاحظات المتعلّقة بهذا العنصر: يمكن الوصول إلى قيمة التاريخ الموجودة حاليًا ضمن هذا العنصر عن طريق الخاصيّة Date له كما سنرى بعد قليل. نوع هذه الخاصيّة مألوف وهو DateTime. توجد خاصيّتان مفيدتان في بعض الأحيان ولكن لم نستخدمهما في هذا التطبيق، وهما MinimumDate لتحديد التاريخ الأدنى الذي لا يمكن الاختيار قبله، و MaximumDate لتحديد التاريخ الأعلى الذي لا يمكن الاختيار بعده. خصّصنا لصيقة بسيطة لعرض النتائج (الأسطر من 44 حتى 51) وصرّحنا أيضًا عن الزر الخاص بإجراء عمليّة الحساب (الأسطر من 53 حتى 57) وأسندناه إلى المتغيّر btnCalculate. أسندنا معالج حدث النقر لهذا الزر في الأسطر من 58 حتى 63 على شكل تعبير Lambda. يحتوي هذا المعالج على شيفرة برمجيّة بسيطة للغاية، فهو يقرأ قيمة الخاصيّة Date من المتغيّر birthDate ويطرحها من التاريخ الحالي ويُسند النتيجة إلى المتغيّر diff الذي سيكون في هذه الحالة من النوع TimeSpan وهي بنية يمكن استخدامها لقياس فترات زمنيّة مُحدّدة (السطر 60) في نهاية الأمر نستخدم الخاصيّة Days من المتغيّر diff للحصول على الفترة الزمنية المطلوبة والتي تمثّل عمر الشخص بالأيّام. قد لا يبدو مظهر هذا التطبيق احترافيًّا كفاية. ولكنّنا سنتعلّم من خلال الدروس القادمة كيفيّة تحسين واجهة المستخدم وجعلها جذّابة من خلال تقنيّات بسيطة نسبيًّا. الخلاصة تعاملنا في هذا الدرس مع بعض العناصر المرئيّة المستخدمة في Xamarin.Forms. توجد بعض العناصر الأخرى التي لم نتحدّث عنها، أو التي لم نعطها حقّها في الحديث عنها. في الواقع أفضّل تأجيل ذلك إلى دروس قادمة لأنّ استخدام بعض هذه العناصر يتطلّب مفاهيم متقدّمة نسبيًّا في Xamarin.Forms. سنبدأ في الدرس التالي تعلّم كيفيّة بناء الواجهات باستخدام رُماز XAML. حيث سنتمكّن من فصل واجهة المستخدم عن الشيفرة البرمجيّة التي تتفاعل معها.
  6. كنّا قد رأينا في دروس سابقة كيفيّة الاستفادة من الخصائص الموجودة في عناصر مرئية في التحكّم في خصائص عناصر مرئيّة أخرى كما فعلنا مسبقًا في هذا الدرس حينما استطعنا التحكّم بحجم الخط المستخدم ضمن لصيقة باستخدام خاصيّة القيمة Value لعنصر Slider. حيث استخدمنا لهذا الغرض معالجات أحداث تحوي شيفرة برمجيّة. سنتعلّم في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms. كيفيّة الاستغناء عن استخدام الشيفرة البرمجيّة في مثل هذه المهام. وذلك من خلال استخدام تقنيّة ربط البيانات data bindings التي تقدّمها لنا Xamarin. تفيد هذه التقنيّة في التقليل من كتابة الشيفرة البرمجيّة إلى حدٍّ كبير. حيث يمكننا ربط العناصر المرئيّة مع بعضها مباشرةً دون الحاجة لكتابة أي سطر من الشيفرة. سنتناول في هذا الدرس مثالين بسيطين لتوضيح هذه الفكرة. سنعمل في المثال الأوّل على التحكّم في حجم لصيقة باستخدام عنصر Slider. أمّا في المثال الثاني فسنجري تحسينًا بسيطًا على المثال الأوّل، حيث سنضيف لصيقة أخرى ونربطها مع نفس عنصر المنزلق Slider، بحيث ستعرض هذه اللصيقة الجديدة القيمة الحاليّة للخاصيّة Value للعنصر Slider. تطبيق التحكّم بحجم اللصيقة ابدأ بإنشاء مشروع جديد من النوع Blank App (Xamarin.Forms Portable) وسمّه SimpleDataBindingApp، ثم أبق فقط على المشروعين SimpleDataBindingApp (Portable) و SimpleDataBindingApp.Droid كما وسبق أن فعلنا في هذا الدرس. بعد ذلك سنضيف صفحة محتوى تعتمد على رُماز XAML كما وسبق أن فعلنا في هذا الدرس سنسمّها SimpleDataBindingPage. احرص على أن تكون محتويات هذه الصفحة على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="SimpleDataBindingApp.SimpleDataBindingPage"> <StackLayout > <StackLayout.Children> <Label x:Name="lblText" VerticalOptions="CenterAndExpand" Text="النص" HorizontalOptions="Center" BindingContext="{x:Reference Name=slider}" Scale="{Binding Path=Value}" /> <Slider x:Name="slider" Maximum="10" Minimum="1" Value="4" VerticalOptions="CenterAndExpand"/> </StackLayout.Children> </StackLayout> </ContentPage> انتقل إلى الملف App.cs واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new SimpleDataBindingPage(); } نفّذ البرنامج باستخدام F5 لتحصل على شكل شبيه بما يلي: يمكنك تمرير عنصر المنزلق Slider لترى كيف يتغيّر حجم اللصيقة تلقائيًّا. هناك عدّة نقاط سنتحدّث عنها حول هذا التطبيق البسيط: 1- لم نستخدم الشيفرة البرمجيّة أبدًا. لقد ربطنا الخاصيّة Value لعنصر Slider مع خاصيّة مقياس العنصر Scale للصيقة من خلال رُماز XAML فحسب. 2- في أيّة عمليّة ربط هناك الكائن المصدر source (في مثالنا السابق كان عنصر Slider)، والكائن الهدف target (في مثالنا السابق كان عنصر اللصيقة Label). 3- في مثالنا السابق، ضبطنا الخاصيّة BindingContext للّصيقة (الهدف) لكي تشير إلى عنصر Slider (المصدر)، لاحظ كيف عيّنّا اسم عنصر المنزلق slider وذلك من خلال النص التالي: BindingContext="{x:Reference Name=slider}" ثم أخبرنا Xamarin أنّنا نريد ربط خاصيّة مقياس العنصر Scale للّصيقة مع الخاصيّة Value لعنصر المنزلق Slider عن طريق ضبط النص التالي إلى الخاصيّة Scale للصيقة: Scale="{Binding Path=Value}" تتحكّم الخاصيّة Scale بمقياس العنصر. سنتحدّث عن ذلك في دروس لاحقة ضمن هذه السلسلة. التطبيق المحسّن للتحكّم بحجم اللصيقة أضف إلى التطبيق السابق صفحة محتوى تعتمد على رُماز XAML كما وسبق أن فعلنا في هذا الدرس سنسمّها EnhancedDataBindingPage. احرص على أن تكون محتويات هذه الصفحة على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="SimpleDataBindingApp.EnhancedDataBindingPage"> <StackLayout > <StackLayout.Children> <Label x:Name="lblText" VerticalOptions="CenterAndExpand" Text="النص" HorizontalOptions="Center" BindingContext="{x:Reference Name=slider}" Scale="{Binding Path=Value}" /> <Slider x:Name="slider" Maximum="10" Minimum="1" Value="4" VerticalOptions="CenterAndExpand"/> <Label x:Name="lblScale" VerticalOptions="CenterAndExpand" Text="{Binding Path=Value, StringFormat = 'Scale = {0:F1}'}" HorizontalOptions="Center" BindingContext="{x:Reference Name=slider}"/> </StackLayout.Children> </StackLayout> </ContentPage> من الواضح أنّ التعديل الوحيد الملاحظ هنا هو في وجود عنصر اللصيقة الجديد lblScale والذي تنحصر وظيفته كما أوضحنا سابقًا على عرض القيمة الحالية لعنصر المنزلق Slider. إليك تعريف عنصر اللصيقة lblScale: <Label x:Name="lblScale" VerticalOptions="CenterAndExpand" Text="{Binding Path=Value, StringFormat = 'Scale = {0:F1}'}" HorizontalOptions="Center" BindingContext="{x:Reference Name=slider}"/> كما وسبق أن فعلنا مع لصيقة النص lblText، قمنا بتعيين الخاصيّة BindingContext لتشير إلى عنصر المنزلق الذي يحمل الاسم "slider": BindingContext="{x:Reference Name=slider}"/> ثمّ سنربط هذه المرّة الخاصيّة Text لهذه اللصيقة بالخاصيّة Value لعنصر المنزلق على الشكل التالي: Text="{Binding Path=Value, StringFormat = 'Scale = {0:F1}'}" لاحظ كيف أنّ الربط Binding قد جرى بين الخاصيّة Value لعنصر المنزلق، والخاصيّة Text لعنصر اللصيقة الجديد lblScale. لاحظ أيضًا الكلمة الجديدة StringFormat. ستُفسَّر هذه الكلمة في الواقع لتصبح استدعاء إلى التابع String.Format الذي يعمل على تنسيق النص Scale = {0:F1} بحيث يظهر ضمن اللصيقة عدد ذو فاصلة عشرية ثابتة تمتلك رقم واحد فقط على يمين الفاصلة العشريّة. يمكنك تغيير هذا السلوك، بحيث تلغي عمليّة التنسيق هذه لترى الفرق وذلك على الشكل التالي: Text="{Binding Path=Value'}" انتقل الآن إلى الملف App.cs واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new EnhancedDataBindingPage(); } عند تنفيذ البرنامج ستحصل على شكل شبيه بما يلي: يمكنك تحريك المنزلق لترى كيف يعمل الربط بين الخاصيّة Value وكل من الخاصيّتين Scale وText للصيقتين الموجودتين في البرنامج. الخلاصة يُعتبر هذا الدرس مثيرًا للانتباه. حيث لم نكتب أي شيفرة برمجيّة في أيّ من التطبيقين اللذين بنيناهما في هذا الدرس. إنّ عمليّة ربط البيانات data bindings بمعزل عن كونها توفّر الوقت والجهد في إنجاز مهام روتينيّة (وهو أمر مهم بالطبع)، تُعتبر حيويةً في أسلوب تصميم التطبيقات MVVM (اختصارًا للكلمات Model View ViewModel) الشائع جدًّا في تصميم تطبيقات الأجهزة المحمولة. سنتناول في الدرس القادم موضوعًا مهمًّا نتعلّم من خلاله كيفيّة تصميم واجهات مستخدم دقيقة من خلال مخطّط الشبكة Grid Layout. وهو أحد أنواع المخطّطات المستخدمة في Xamarin والتي من المؤكّد أنّها ستثير إعجابك.
  7. يُعتبر مخطّط الشبكة Grid Layout من المخطّطات الهامّة والضروريّة في تصميم واجهات المستخدم بالنسبة لتطبيقات الأجهزة المحمولة. فيمكن من خلال هذا المخطّط التحكّم على نحو دقيق نسبيًّا بمواقع جميع العناصر المرئيّة الموجودة في الواجهة، وذلك بخلاف مخطّط المكدّس Stack Layout الذي تناولناه منذ بداية هذه السلسلة حتى الآن. حيث كان من الصعوبة التحكّم بمواقع العناصر المرئيّة بشكل دقيق، وخاصةً فيما يتعلّق بالمحاذاة. في الحقيقة يمكن استخدام كل من هذين المخطّطين معًا في الواجهة الواحدة. سنبدأ في هذا الدرس بالتعرّف على مفهوم هذا المخطّط وطريقة استخدامه. ثمّ نُتْبع هذا الدرس بدرسين آخرين يتناولان تطبيقين عمليين لاكتساب الخبرة اللازمة للعمل مع مخطّط الشبكة بسهولة ويسر أكبر. ما هو مخطّط الشبكة؟ مخطّط الشبكة هو عبارة عن بنية تشبه الجدول من الناحية الشكلية ولكنّها تختلف عنه من الناحية العمليّة. فمخطّط الشبكة قد صُمّم بالأساس ليكون وسيلة لتنظيم العناصر المرئيّة ضمن واجهة المستخدم. أمّا الجدول فهو بنية وظيفتها احتواء بيانات يمكن عرضها للمستخدم. سيكون أغلب تعاملنا مع مخطّط الشبكة من خلال رُماز XAML. إذ أنّه من الممكن أيضًا استخدام الأسلوب البرمجيّ في التعامل معه. ولكن التعامل معه من خلال الرُماز أفضل من ناحية الاستخدام والتنظيم. يمكن تعريف مخطّط الشبكة من خلال الوسم . حيث من الممكن تعيين الأسطر rows والأعمدة columns التي يتألّف منها هذا المخطّط من خلال الخاصيّتين RowDefinitions التي تحتوي على تعاريف الأسطر وColumnDefinitions التي تحتوي على تعاريف الأعمدة. يُدعى مكان تقاطع السطر والعمود بالخليّة Cell. الخاصيّة RowDefinitions هي عبارة عن مجموعة collection من العناصر التي يكون كلّ منها من النوع RowDefinition وتعني تعريف سطر. يمكن من خلال هذا النوع ضبط العديد من خصائص هذا السطر. سنهتم حاليًّا بالخاصيّة Height التي تُعبّر عن ارتفاع السطر. من الممكن تعيين قيمة هذا الارتفاع بثلاثة أنواع من المقاييس: النوع الأوّل هو القيمة العدديّة المباشرة وهي قيمة لا تتعلّق لا تتعلّق بالشاشة device-independent units (انظر هذا الدرس). النوع الثاني هو الكلمة Auto، والتي تعني أنّه ينبغي أن يكون ارتفاع السطر مساويًا لأعلى ارتفاع عنصر مرئي موجود ضمنه. النوع الثالث هو النوع النسبيّ، ويُعبّر عنه رمز النجمة (*)، حيث يكون ارتفاع السطر في هذه الحالة بحيث يشغل باقي الارتفاع المتاح لمخطّط الشبكة ككل. ومن الممكن أن يتعلّق أيضًا بسطر آخر له نفس هذا النوع من القياس، وسنوضّح ذلك في مثال تطبيقي بعد قليل. أمّا بالنسبة للخاصيّة ColumnDefinitions هي عبارة عن مجموعة collection من العناصر التي يكون كلّ منها من النوع ColumnDefinition وتعني تعريف عمود. يمكن من خلال هذا النوع ضبط العديد من خصائص هذا العمود. سنهتم بالخاصيّة Width منه التي تُعبّر عن عرض العمود. من الممكن تعيين قيمة هذا العرض كما هو الحال مع تعريف السطر أيضًا بثلاثة أنواع من المقاييس، تماثل من حيث المبدأ الأنواع الثلاثة السابقة من القياس للسطر: النوع الأوّل هنا هو نفسه النوع الأوّل بالنسبة للسطر، وهو قيمة العدديّة المباشرة التي لا تتعلّق لا تتعلّق بالشاشة device-independent units. النوع الثاني هو الكلمة Auto، والتي تعني أنّه ينبغي أن يكون عرض العمود مساويًا لأعرض عنصر مرئي موجود ضمنه. النوع الثالث هو النوع النسبيّ، ويُعبّر عنه رمز النجمة (*) أيضًا، حيث يكون عرض العمود في هذه الحالة بحيث يشغل باقي العرض المتاح لمخطّط الشبكة ككل. ومن الممكن أن يتعلّق أيضًا بعمود آخر له نفس هذا النوع من القياس. وسنوضّح ذلك أيضًا في المثال التطبيقي الذي سنكتبه بعد قليل. من الجدير ذكره أنّه في حالة القياس النسبيّ في كلّ من حالة السطر والعمود، من الممكن وجود رقم بجوار رمز النجمة يدل على الحصّة التناسبيّة للسطر أو للعمود كما سنرى ذلك بعد قليل. تطبيق عملي بسيط باستخدام مخطّط الشبكة ابدأ بإنشاء مشروع جديد من النوع Blank App (Xamarin.Forms Portable) وسمّه SimpleGridApp، ثم أبق فقط على المشروعين SimpleGridApp (Portable) و SimpleGridApp.Droid كما وسبق أن فعلنا في هذا الدرس. بعد ذلك سنضيف صفحة محتوى تعتمد على رُماز XAML كما وسبق أن فعلنا في هذا الدرس سنسمّها SimpleGridPage. احرص على أن تكون محتويات هذه الصفحة على الشكل التالي: 1 <?xml version="1.0" encoding="utf-8" ?> 2 <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 3 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 4 x:Class="SimpleGridApp.SimpleGridPage"> 5 <Grid> 6 <Grid.RowDefinitions> 7 <RowDefinition Height="Auto" /> 8 <RowDefinition Height="100" /> 9 <RowDefinition Height="2*" /> 10 <RowDefinition Height="1*" /> 11 </Grid.RowDefinitions> 12 13 <Grid.ColumnDefinitions> 14 <ColumnDefinition Width="*" /> 15 <ColumnDefinition Width="*" /> 16 </Grid.ColumnDefinitions> 17 18 <Label Text="مخطط الشبكة" 19 Grid.Row="0" Grid.Column="0" 20 FontSize="Large" 21 HorizontalOptions="End" /> 22 23 <Label Text="تجربة مخطط الشبكة" 24 Grid.Row="0" Grid.Column="1" 25 FontSize="Small" 26 HorizontalOptions="End" 27 VerticalOptions="End" /> 28 29 <Image BackgroundColor="Gray" 30 Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Source="icon.png" /> 31 32 <BoxView Color="Green" 33 Grid.Row="2" Grid.Column="0" /> 34 35 <BoxView Color="Red" 36 Grid.Row="2" Grid.Column="1" Grid.RowSpan="2" /> 37 38 <BoxView Color="Blue" 39 Opacity="0.5" 40 Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" /> 41 </Grid> 42 </ContentPage> انتقل الآن إلى الملف App.cs واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new SimpleGridPage(); } نفّذ البرنامج لتحصل على شكل شبيه بما يلي: لنحلّل الآن هذا التطبيق البسيط. في البداية أنشأنا مخطّط شبكة عن طريق كتابة الوسم اعتبارًا من السطر 5. بعد ذلك وضعنا تعاريف أسطر الشبكة من السطر 6 حتى السطر 10 على الشكل التالي: <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="100" /> <RowDefinition Height="2*" /> <RowDefinition Height="1*" /> </Grid.RowDefinitions> ويعني ذلك أنّ هذه الشبكة سيكون لها أربعة أسطر (لوجود أربعة وسوم RowDefinition). سيكون ارتفاع السطر الأوّل Auto، ويعني ذلك أنّ هذا السطر سيكون ارتفاعه مساويًا لارتفاع أعلى عنصر موجود ضمنه. أمّا السطر الثاني فارتفاعه 100 وحدة مستقلة عن الجهاز. بالنسبة للسطرين الثالث والرابع فسيكون ارتفاعهما نسبيًّا لوجود رمز النجمة *. لاحظ وجود الرقم 2 بجوار رمز النجمة بالنسبة للسطر الثالث، ووجود الرقم 1 بجوار رمز النجمة بالنسبة للسطر الرابع. الذي سيحدث هنا أنّه وبعد أن يتم تعيين ارتفاعيّ السطرين الأوّل والثاني سيتم اقتسام الارتفاع المتبقي للشبكة بين السطرين الثالث والرابع بحيث تكون حصّة السطر الثالث ضعف (الرقم 2) حصّة السطر الرابع (الرقم 1). بعبارة أخرى ارتفاع السطر الثالث سيكون 2 إلى 1 من ارتفاع السطر الرابع. وضعنا بعد ذلك تعاريف أعمدة الشبكة من السطر 13 حتى السطر 16 على الشكل التالي: <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> من الواضح أنّه سيكون لهذه الشبكة عمودان فقط. كل من هذين العمودين سيكون عرضه نسبيّ (الرمز *). ولكن بسبب عدم وجود أيّ رقم بجوار رمز النجمة في كلّ منهما، سيتمّ اعتبار وجود الرقم 1 بشكل افتراضيّ بجوار كل رمز نجمة (مثل1*). أي بالمحصلة سيكون لكل منهما نفس الرقم، وبالتالي نفس الحصّة النسبيّة لذلك سيكون لهما نفس العرض تمامًا. تأتي بعد ذلك العناصر المرئيّة التي سنضعها ضمن هذه الشبكة. يبدأ ترقيم كل من الأسطر والأعمدة بالرقم صفر. فالسطر رقم صفر هو السطر الأوّل، وكذلك الأمر بالنسبة للأعمدة. إذا نظرت إلى الأسطر من 18 حتى 21 فستجد أنّنا نضيف لصيقة إلى الخليّة المحدّدة بالسطر الأوّل والعمود الأوّل. ويكمن سبب ذلك في السطر 19 حيث نجد الخاصيّتين Grid.Row="0" للسطر رقم 0، وGrid.Column="0" للعمود رقم 0 أيضًا. أي السطر الأوّل والعمود الأوّل. بنفس الأسلوب تمامًا نضيف لصيقة أخرى إلى الخليّة المحدّدة بالسطر الأوّل والعمود الثاني (الأسطر من 23 حتى 27)، وأيضًا عنصر صورة ضمن الخليّة المحدّدة بالسطر الثاني والعمود الأوّل (السطران 29 و30). نلاحظ أيضًا أنّ عنصر الصورة هذا سيشغل بالإضافة إلى العمود الأوّل العمود الثاني أيضًا، وذلك بسبب وجود الخاصيّة Grid.ColumnSpan="2" والتي تشير إلى أنّ الخليّة التي ستحتوي عنصر الصورة يجب أن تمتد على عمودين متتاليين باتجاه اليمين، وهذا ما يُفسّر سبب ظهورها ممتدّة على كامل عرض الشبكة كما هو واضح في الشكل السابق. بعد إضافة عنصر الصورة السابق، سنضيف ثلاثة عناصر BoxView لأغراض العرض فقط (الأسطر من 32 حتى 40). حيث ستشغل هذه العناصر الثلاثة على الترتيب الخلايا المحدّدة بالسطر الثالث والعمود الأوّل، ثم السطر الثالث والعمود الثاني، ثم السطر الرابع والعمود الأوّل. مع ملاحظة أنّ عنصر BoxView الأوسط يحوي الخاصيّة Grid.RowSpan="2" التي تسمح للخلية التي تحتويه بالامتداد إلى سطرين متتاليين. أمّا عنصر BoxView الأخير فيحوي على الخاصية Grid.ColumnSpan="2" لتسمح له بالامتداد على عمودين متتاليين. أي أنّ عنصريّ BoxView الثالث والرابع سيتداخلان في الخليّة المحدّدة بالسطر الرابع والعمود الثاني كما هو واضح من الشكل السابق. الخلاصة تعلّمنا في هذا الدرس كيفيّة التعامل الأساسي مع مخطّط الشبكة Grid Layout. ولعلّ الفهم الأفضل لمثل هذه الأمور تنبع من الممارسّة العمليّة ببناء تطبيقات عمليّة صغيرة ولكنّها مفيدة. سنعمل في الدرسين القادمين على بناء تطبيقين بسيطين يحتويان على العديد من التقنيّات البرمجيّة المفيدة والتي من الممكن استخدامها في التطبيقات الواقعيّة.
  8. سنتناول في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms تطبيقًا عمليًّا مفيدًا حول التعامل مع مخطّط الشبكة Grid Layout وهو عبارة عن تطبيق لوحة أرقام يمكن تطويرها لاحقًا لتصبح مثلًا تطبيق آلة حاسبة متعدّد الوظائف. اطلعنا في الدرس السابق على مخطّط الشبكة وتعلمنا بعض المبادئ الأساسيّة لاستخدامه. يمكنك مراجعة ذلك الدرس، أو من الممكن أن تبدأ مباشرةً في هذا الدرس. فكرة التطبيق سنعمل على كتابة تطبيق لوحة أرقام بسيط، حيث ستظهر أزرار تحمل الأرقام من 0 حتى 9 بالإضافة إلى زر الفاصلة العشرية، مع وجود زر لإلغاء آخر رقم تمّت كتابته. ستبدو واجهة التطبيق كما في الشكل التالي: عندما ينقر المستخدم على أزرار الأرقام ستظهر هذه الأرقام تباعًا بجوار بعضها بالترتيب. أمّا عندما ينقر زر الإلغاء فسيتم إلغاء آخر رقم تمّ نقره. لنستعرض الشيفرة البرمجيّة لهذا التطبيق في الفقرة التالية. الشيفرة البرمجيّة للتطبيق ابدأ بإنشاء مشروع جديد من النوع Blank App (Xamarin.Forms Portable) وسمّه KeypadGrid، ثم أبق فقط على المشروعين KeypadGrid (Portable) و KeypadGrid.Droid كما وسبق أن فعلنا في هذا الدرس. بعد ذلك سنضيف صفحة محتوى تعتمد على رُماز XAML كما وسبق أن فعلنا في هذا الدرس سنسمّها KeypadGridPage. احرص على أن تكون محتويات هذه الصفحة على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="KeypadGrid.KeypadGridPage"> <Grid RowSpacing="2" ColumnSpacing="2" VerticalOptions="Center" HorizontalOptions="Center" WidthRequest="180" Grid.HeightRequest="300"> <Label x:Name="displayLabel" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" FontSize="Large" LineBreakMode="HeadTruncation" VerticalOptions="Center" HorizontalTextAlignment="End" /> <Button x:Name="btnBackspace" Text="⇦" Grid.Row="0" Grid.Column="2" IsEnabled="False" Clicked="btnBackspace_Clicked" /> <Button Text="7" StyleId="7" Grid.Row="1" Grid.Column="0" Clicked="OnDigitButtonClicked" /> <Button Text="8" StyleId="8" Grid.Row="1" Grid.Column="1" Clicked="OnDigitButtonClicked" /> <Button Text="9" StyleId="9" Grid.Row="1" Grid.Column="2" Clicked="OnDigitButtonClicked" /> <Button Text="4" StyleId="4" Grid.Row="2" Grid.Column="0" Clicked="OnDigitButtonClicked" /> <Button Text="5" StyleId="5" Grid.Row="2" Grid.Column="1" Clicked="OnDigitButtonClicked" /> <Button Text="6" StyleId="6" Grid.Row="2" Grid.Column="2" Clicked="OnDigitButtonClicked" /> <Button Text="1" StyleId="1" Grid.Row="3" Grid.Column="0" Clicked="OnDigitButtonClicked" /> <Button Text="2" StyleId="2" Grid.Row="3" Grid.Column="1" Clicked="OnDigitButtonClicked" /> <Button Text="3" StyleId="3" Grid.Row="3" Grid.Column="2" Clicked="OnDigitButtonClicked" /> <Button Text="0" StyleId="0" Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Clicked="OnDigitButtonClicked" /> <Button Text="." StyleId="." Grid.Row="4" Grid.Column="2" Clicked="OnDigitButtonClicked" /> </Grid> </ContentPage> من السهل فهم الرُماز السابق مع بعض الملاحظات البسيطة: في البداية لاحظ أنّنا قد أسندنا خصائص إضافيّة لمخطّط الشبكة مثل تباعد الأسطر RowSpacing وتباعد الأعمدة ColumnSpacing وذلك بهدف جعل الأزرار غير ملتصقة ببعضها البعض. كما أسندنا الخيارات الرأسية VerticalOptions والأفقية HorizontalOptions لكي تحمل كل منهما القيمة Center بهدف جعل الشبكة تظهر في مركز الشاشة. لم نستخدم في هذا التطبيق الخاصيّتين RowDefinitions و ColumnDefinitions اللّتان تحدثنا عنهما في الدرس السابق. إذ أنّه من الممكن اعتماد الإعدادات الافتراضيّة الخاصّة بهما، والبدء مباشرة بإضافة العناصر المرئيّة، حيث يمكن من خلال هذه العناصر تحديد السطر والعمود للخليّة التي نودّ وضع العنصر ضمنها كما سنرى بعد قليل. اللصيقة الوحيدة الموجودة في هذا التطبيق تُستَخدم لعرض الرقم الذي أدخله المستخدم. لاحظ أنّنا نريد عرضها في الخلية المحدّدة بالسطر الأوّل والعمود الأوّل، بالإضافة إلى الانتشار لعمودين متتاليين بسبب وجود الخصائص التالية: Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2". كما توجد خاصية أخرى ملفتة للنظر لهذه اللصيقة، وهي الخاصيّة LineBreakMode والتي تحمل القيمة HeadTruncation. هذه القيمة مفيدة في مثل هذه التطبيقات حيث يؤدّي وجودها إلى اقتطاع النص الذي تمّ إدخاله أولًا في حال لم تتسع اللصيقة لعرض كامل النص (يمكنك تجريب ذلك بنفسك فيما بعد). الزر الأوّل الذي يأتي بعد تعريف اللصيقة هو زر إلغاء آخر رقم مُدخل، واسمه btnBackspace، وهو مرتبط مع معالج حدث النقر btnBackspace_Clicked الموجود ضمن ملف الشيفرة البرمجيّة كما سنرى بعد قليل. بالنسبة لبقيّة الأزرار فأمرها بسيط. فكلّ زر نُعيّن له الخاصيّة StyleId للاحتفاظ بالقيمة النصيّة الموافقة للرقم الذي يحمله، فمثًلا الزر الذي يُعبّر عن الرقم 9 ستكون قيمة StyleId له تساوي "9"، وهكذا. الجدير بالملاحظة هنا أننا قد ربطنا جميع هذه الأزرار بنفس معالج الحدث وهو OnDigitButtonClicked، وهو أمر جائز تمامًا، بل ومن المفضّل القيام به. سنطّلع على هذا المعالج بعد قليل. انتقل الآن إلى ملف الشيفرة البرمجيّة KeypadGridPage.xaml.cs الموافق لملف الرُماز السابق، واحرص على أن تكون محتوياته على الشكل التالي: using System; using Xamarin.Forms; namespace KeypadGrid { public partial class KeypadGridPage : ContentPage { public KeypadGridPage() { InitializeComponent(); btnBackspace.IsEnabled = displayLabel.Text != null && displayLabel.Text.Length > 0; } void OnDigitButtonClicked(object sender, EventArgs args) { Button button = (Button)sender; displayLabel.Text += (string)button.StyleId; btnBackspace.IsEnabled = true; } void btnBackspace_Clicked(object sender, EventArgs args) { string text = displayLabel.Text; displayLabel.Text = text.Substring(0, text.Length - 1); btnBackspace.IsEnabled = displayLabel.Text.Length > 0; } } } الشيفرة السابقة بسيطة للغاية، فالصنف KeypadGridPage يحتوي بشكل أساسي على معالجيّ لحدث النقر: المعالج الأوّل هو OnDigitButtonClicked وهو يُستدعى عندما ينقر المستخدم على أحد أزرار الأرقام بالإضافة إلى زر الفاصلة العشرية. وفي هذا المعالج يتم إضافة الرقم الذي نقره المستخدم توًّا إلى محتويات اللصيقة التي تعرض كامل الرقم المُدخَل. نبدأ أولًا بعمليّة تحويل cast للحصول على الزر الذي سبّب استدعاء هذا المعالج: Button button = (Button)sender; ثمّ استخلاص قيمة الخاصيّة StyleId له وضمّها إلى اللصيقة displayLabel: displayLabel.Text += (string)button.StyleId; ومن ثمّ تفعيل زر إلغاء آخر رقم بسبب أنّه قد أصبح هناك رقم واحد على الأقل بشكل مؤكّد ضمن اللصيقة. أمّا المعالج الثاني فهو btnBackspace_Clicked وهو يُستَدعى عندما يتم النقر على زر إلغاء آخر رقم مُدخَل. الشيفرة البرمجيّة ضمن هذا المعالج بسيطة، فهي تعمل أولًا على الحصول على النص الموجود أصلًا ضمن اللصيقة displayLabel وتضعه ضمن المتغيّر النصي text: string text = displayLabel.Text; ومن ثمّ تقوم بحذف آخر رقم عن طريق التابع Substring من المتغيّر text ومن ثمّ إعادة إسناد النص الناتج إلى الخاصيّة Text للصيقة displayLabel. displayLabel.Text = text.Substring(0, text.Length - 1); نقوم في نهاية المطاف بتفعيل زر إلغاء آخر رقم مُدخل أو تعمل على إلغاء تفعيله باستخدام طريقة جميلة تعتمد على تقييم التعبير المنطقي الموجود على يمين عامل الإسناد (=): btnBackspace.IsEnabled = displayLabel.Text.Length > 0; فإذا كانت اللصيقة تحتوي على رقم واحد على الأقل سيكون التعبير displayLabel.Text.Length > 0 مساويًا للقيمة true وبالتالي يتم تفعيل الزر. وإلّا سيعطي هذا التعبير القيمة false ويتم إلغاء تفعيله. حاول إجراء بعض التجارب على هذا التطبيق، ثمّ فكّر كيف يمكن تعديله لإنشاء تطبيق آلة حاسبة فعليّ، يمكنه دعم العمليّات الحسابيّة الأساسيّة الأربع على سبيل المثال. الخلاصة استفدنا من التطبيق الذي تناولناه في هذا الدرس، في كيفيّة استخدام مخطّط الشبكة بشكل عمليّ بغية الاستفادة من مزايا التموضع الدقيقة نسبيًّا التي يوفّرها للعناصر المرئيّة التي تُوضَع ضمنه. سنتابع العمل في الدرس التالي لنبني تطبيقًا عمليًّا مفيدًا أخرًا يستفيد أيضًا من المزايا الرائعة لمخطّط الشبكة في إنشاء مخطّط تكراري إحصائي يشبه مخطّطات الأعمدة التي من الممكن الحصول عليها من تطبيقات مكتبيّة مثل Microsoft Excel مثلًا.
  9. سنتناول في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms تطبيقًا عمليًّا مفيدًا آخرًا، نتعلّم من خلاله المزيد عن مزايا مخطّط الشبكة Grid Layout واستخداماته العمليّة في التطبيقات التي نطوّرها. سيستخدم هذا التطبيق مخطّطًا تكراريًّا Histogram سيكون مصمّمًا باستخدام مخطّط الشبكة. إذا أردت معرفة المزيد عن مخطّط الشبكة فيمكنك مراجعة هذا الدرس. وإلّا، فيمكنك البدء مباشرةً في هذا الدرس. فكرة التطبيق تتلخّص فكرة هذا التطبيق في أنّ المستخدم سيُدخل نصًّا بأيّ لغة يرغبها. ثم ينقر على زر "احسب" ليعمل التطبيق بعد ذلك على حساب تكرار كلّ محرف من محارف النص المُدخل. بعد الانتهاء من ذلك، سيعمل التطبيق على إظهار مخطّط تكراري Histogram على شكل مخطّط أعمدة bars chart يمكن من خلاله ملاحظة الأحرف الأكثر تكرارًا والأقل تكرارًا من نظرة واحدة. سيعتمد التطبيق على عنصر رسومي من النوع Editor لاستقبال الدخل النصيّ من المستخدم. وكذلك على عنصر الزر Button للبدء بإيجاد القيم التكراريّة، وأيضًا سيستخدم مخطط شبكة لعرض النتائج التكراريّة. من أجل كل حرف موجود في النص، سيضيف التطبيق عمود bar سيكون عبارة عن عنصر BoxView يكون ارتفاعه متناسبًا مع تكرار الحرف ضمن النص المُدخَل. بعد إظهار مخطّط الأعمدة سيكون من الممكن النقر على أي عمود (عنصر BoxView) ليعرض التطبيق رسالة للمستخدم تحوي الحرف الذي يمثّله هذا العمود مع عدد مرًّات تكراره ضمن النص المُدخَل. أي بشكل مشابه لما يلي: سنتناول في الفقرة التالية كامل الشيفرة البرمجيّة لهذا التطبيق. الشيفرة البرمجيّة للتطبيق ابدأ بإنشاء مشروع جديد من النوع Blank App (Xamarin.Forms Portable) وسمّه GridBarChart، ثم أبق فقط على المشروعين GridBarChart (Portable) و GridBarChart.Droid كما وسبق أن فعلنا في هذا الدرس. بعد ذلك سنضيف صفحة محتوى تعتمد على رماز XAML كما وسبق أن فعلنا في هذا الدرس سنسمّها GridBarChartPage. احرص على أن تكون محتويات هذه الصفحة على الشكل التالي: 1 <?xml version="1.0" encoding="utf-8" ?> 2 <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 3 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 4 x:Class="GridBarChart.GridBarChartPage"> 5 <StackLayout> 6 <StackLayout VerticalOptions="Start"> 7 <Label Text="تطبيق حساب تكرار المحارف" 8 HorizontalOptions="CenterAndExpand" 9 HorizontalTextAlignment="Center" 10 FontSize="Large" 11 TextColor="Green"/> 12 <Editor x:Name="txtInput" /> 13 <Button x:Name="btnCalculate" 14 Text="احسب" 15 HorizontalOptions="FillAndExpand" 16 Clicked="btnCalculate_Clicked" /> 17 </StackLayout> 18 19 <Grid x:Name="grdLettersHistogram" 20 VerticalOptions="FillAndExpand" 21 BackgroundColor="#EBF2F5" 22 Padding="5,5,5,0"> 23 </Grid> 24 </StackLayout> 25 </ContentPage> من الملاحظ أنّ رماز XAML لهذا التطبيق صغير نسبيًّا. يبدأ الرماز بتعريف مخطّط مكدّس StackLayout سيعمل على احتواء كامل واجهة التطبيق. سيحتوي مخطّط المكدّس هذا على عنصرين آخرين: العنصر الأوّل عبارة عن مخطّط مكدّس آخر (الأسطر من 6 حتى 17) يحتوي على لصيقة تعريفيّة بالتطبيق، وعلى عنصر الإدخال النصيّ Editor الذي أسميته txtInput وهو يسمح بكتابة أكثر من سطر نصيّ واحد ضمنه، بالإضافة إلى زر البدء بالحساب الذي أسميته btnCalculate والذي سيؤدّي نقره إلى الانتقال إلى معالج حدث النقر btnCalculate_Clicked. المعالج السابق سيكون بالتأكيد ضمن ملف الشيفرة البرمجيّة الذي سنطّلع عليه بعد قليل. العنصر الثاني هو مخطّط الشبكة (الأسطر من 19 حتى 23) والذي أسميته grdLettersHistogram. لاحظ أنّ هذا المخطّط لا يحوي حاليًّا أيّة عناصر. سنضيف لاحقًا إلى هذا المخطّط عناصر BoxView يمثّل كل منها محرفًا موجودًا ضمن النص المُدخَل، وذلك عن طريق الشيفرة البرمجيّة كما سنرى بعد قليل. انتقل الآن إلى الملف GridBarChartPage.xaml.cs وهو ملف الشيفرة البرمجيّة التابع لملف رماز XAML السابق. احرص على أن تكون محتوياته على الشكل التالي: 1 using System; 2 using System.Collections.Generic; 3 using Xamarin.Forms; 4 5 namespace GridBarChart 6 { 7 public partial class GridBarChartPage : ContentPage 8 { 9 public GridBarChartPage() 10 { 11 InitializeComponent(); 12 } 13 14 private void btnCalculate_Clicked(object sender, EventArgs e) 15 { 16 Dictionary<char, int> freq = new Dictionary<char, int>(); 17 List<View> bars = new List<View>(); 18 TapGestureRecognizer tapGesture = new TapGestureRecognizer(); 19 int maxFreq = -1; 20 21 tapGesture.Tapped += TapGesture_Tapped; 22 23 string s = txtInput.Text; 24 25 foreach (char t in s) 26 { 27 if (char.IsLetterOrDigit(t)) 28 { 29 if (!freq.ContainsKey(t)) 30 { 31 freq.Add(t, 1); 32 } 33 else 34 { 35 freq[t] = ++freq[t]; 36 } 37 } 38 } 39 40 grdLettersHistogram.Children.Clear(); 41 42 //find the max letter frequency. 43 foreach(var v in freq.Values) 44 { 45 if(maxFreq< v) 46 { 47 maxFreq = v; 48 } 49 } 50 51 //calculate measurment unit will apply on bars. 52 double measureUnit = grdLettersHistogram.Height / maxFreq; 53 54 foreach (var c in freq.Keys) 55 { 56 var boxView = new BoxView 57 { 58 HeightRequest = measureUnit * freq[c], 59 BackgroundColor = Color.Accent, 60 VerticalOptions = LayoutOptions.End, 61 StyleId = string.Format("المحرف '{0}' له التكرار {1}", c, freq[c]), 62 }; 63 64 boxView.GestureRecognizers.Add(tapGesture); 65 66 bars.Add(boxView); 67 } 68 69 grdLettersHistogram.Children.AddHorizontal(bars); 70 } 71 72 private void TapGesture_Tapped(object sender, EventArgs e) 73 { 74 DisplayAlert("رسالة", ((BoxView)sender).StyleId, "اغلاق"); 75 } 76 } 77 } تكمن معظم الشيفرة البرمجيّة في هذا الملف ضمن معالج حدث نقر زر حساب التكرارات btnCalculate_Clicked الذي يمتد من السطر 14 حتى السطر 70. في البداية تمّ تعريف المتغيّر freq في السطر 16 ليكون من النوع العمومي Dictionary، وهو عبارة عن بنية معطيات مشهورة تُعرف بالقاموس dictionary. سيحتوي هذا المتغيّر على جميع الأحرف المختلفة الموجودة ضمن النص المُدخَل بالإضافة إلى عدد مرّات التكرار لكلّ حرف. أمّا في السطر 17 فقد تمّ تعريف المتغيّر bars وهو من نوع القائمة List. سيحتوي هذا المتغيّر على جميع عناصر BoxView التي سننشئها لاحقًا من الشيفرة البرمجيّة. بعد ذلك نعرّف المتغيّر tapGesture وهو من النوع TapGestureRecognizer. سنتمكّن من خلال هذا المتغيّر أن نُكسِب لأي عنصر BoxView إمكانيّة أن ينقر عليه المستخدم ليعرض عند ذلك الرسالة التي تحدثنا عنها مسبقًا. أمّا في السطر 19 فقد عرّفنا المتغيّر maxFreq الذي سيحوي التكرار الأكبر من بين الحروف المختلفة.,> ,> نعمل في السطر 21 على إضافة معالج الحدث TapGesture_Tapped (مصرّح عنه في الأسطر من 72 حتى 75) لحدث النقر Tapped للكائن tapGesture. بعد أن نحصل على دخل المستخدم ونضعه ضمن المتغيّر s في السطر 23، سندخل حلقة foreach التكراريّة بحيث نقرأ محتويات النص s محرفًا تلو الآخر، ونختبر كون هذا المحرف هو حرف أو رقم في السطر 27 لأنّنا لا نريد قبول الفراغات على سبيل المثال. في السطر 29 سنختبر في حال كان هذا المحرف موجود فعلًا ضمن القاموس freq من قبل أم لا وذلك عن طريق التابع ContainsKey. فإن كان غير موجود من قبل، سنعمل على إضافته في السطر 31، أمّا إذا كان موجود من قبل فسنعمل على إضافة التكرار الخاص به بمقدار واحد وذلك في السطر 35: freq[t] = ++freq[t]; لاحظ أنّ التعبير freq[t] يسمح لنا بالوصول إلى قيمة المفتاح t الموجودة في القاموس freq. بعد ذلك سنمسح محتويات مخطّط الشبكة في السطر 40، ثم سننتقل إلى حلقة foreach أخرى (الأسطر من 43 حتى 49) تسمح لنا بحساب التكرار الأكبر من بين المحارف الموجودة ضمن القاموس freq. في السطر 52 سنقوم بإجراء عمليّة حسابيّة بسيطة، حيث سنعمل على حساب واحدة القياس الخاصّة بالأعمدة (عناصر BoxView)، وبشكل أدق، واحدة القياس الخاصّة بارتفاع كل عنصر BoxView. وذلك من خلال تقسيم ارتفاع مخطّط الشبكة grdLettersHistogram على التكرار الأكبر maxFreq الذي حسبناه قبل قليل. سنخزّن واحدة القياس هذه ضمن المتغيّر measureUnit كما هو واضح. سندخل أخيرًا ضمن حلقة foreach أخرى (الأسطر من 54 حتى 67) وظيفتها إنشاء عناصر BoxView كلّ منها يمثّل محرف مختلف موجود ضمن القاموس freq، وبارتفاع يتناسب مع تكرار هذا المحرف ضمن النص المُدخل. ستعمل هذه الحلقة على إضافة كل عنصر BoxView إلى القائمة bars (السطر 66). تجدر الملاحظة أنّنا في السطر 61 قد أسندنا نصًّا توضيحيًّا إلى الخاصيّة StyleId لعنصر BoxView. يحتوي هذا النص على الحرف وعلى تكراره ضمن النص المُدخَل. في الواقع يمكننا دومًا استخدام الخاصيّة StyleId مع أيّ عنصر يرث من الصنف View لمثل هذه الأغراض. في النهاية سنعمل على إضافة الأعمدة (عناصر BoxView) الموجودة ضمن القائمة bars دفعةً واحدةً إلى مخطّط الشبكة من خلال العبارة التالي الموجودة في السطر 69: grdLettersHistogram.Children.AddHorizontal(bars); يعمل التابع AddHorizontal من الخاصّية Children لمخطّط الشبكة على إضافة عنصر أو مجموعة من العناصر (كما في مثالنا) إلى مخطّط الشبكة على أن تكون على نفس السطر (أفقيًّا horizontal) وذلك دفعةً واحدةً. يوجد في الواقع تابع آخر مماثل لهذا التابع وهو التابع AddVertical، ولكنّه يضيف العناصر على نفس العمود. من الممكن تحديد السطر أو العمود المراد الإضافة إليه عن طريق توابع ساكنة static مثل Grid.SetRow و Grid.SetColumn ولكنّنا لن نحتاج إلى مثل هذه التوابع لأنّنا سنترك الإعدادات الافتراضيّة كما هي. بقي أن نتحدّث عن معالج حدث النقر TapGesture_Tapped. في الحقيقة الشيء الوحيد الذي يفعله هذا الحدث هو إظهار رسالة تنبيه للمستخدم (السطر 74) عند نقره لأحد الأعمدة ضمن المخطّط (عنصر BoxView) تحتوي هذه الرسالة على النص الذي مرّرناه مسبقًا لعنصر BoxView ضمن الخاصيّة StyleId له. انتقل الآن إلى الملف App.cs واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new GridBarChartPage(); } نفّذ البرنامج وأجر بعض الإدخالات النصيّة، وراقب النتائج. حاول نقر بعض الأعمدة لتظهر لك رسالة المعلومات حوله. الخلاصة تناولنا في هذا الدرس تطبيقًا عمليًّا مفيدًا في حساب التكرارات للأحرف ضمن نص ما، ومن ثمّ عرضها بشكل مخطّط تكراري histogram ضمن مخطّط الشبكة. يوضّح لنا هذا التطبيق مدى القوّة والمرونة التي من الممكن الحصول عليها من استخدام مخطّط الشبكة، هذا بالإضافة إلى إمكانيّة المزج بين استخدام مخطّط الشبكة ومخطّط المكدّس بكلّ سهولة ويسر. هناك الكثير من التطبيقات التي يمكن استخدام مخطّط الشبكة معها. وسنعمل في الدروس القادمة على استخدامه بشكل متكرّر في التطبيقات التي نطوّرها.
  10. المخطّط المطلق Absolute Layout هو من المخطّطات المعتمدة في Xamarin، وتقوم فكرته على السماح للعناصر المرئيّة الموجودة ضمنه بأن يكون تموضعها بشكل مطلق، أي من الممكن ضبط مكان التموضع بالإضافة إلى عرض وارتفاع كل عنصر مرئي بشكل دقيق باستخدام الواحدات المستقلة عن الجهاز independent-device units. ولعلّ هذا المفهوم ليس جديدًا، بل هو من المفاهيم القديمة في عالم البرمجة، فقد كان المبرمجون قديمًا (وما زالوا في بعض الأحيان حتى اليوم) يعتمدون على بناء واجهات التطبيقات بتعريف موضع وأبعاد (العرض والارتفاع) كل عنصر بشكل دقيق ضمن الواجهة. سنطّلع في هذا الدرس ضمن سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms على كيفيّة التعامل مع هذا المخطّط باستخدام رماز XAML من خلال تطبيقين بسيطين. تطبيق المربّعات المتداخلة تعتمد فكرة هذا التطبيق على وجود 3 مربّعات متداخلة فيما بينها بحيث يمتلك كلّ منها شفافيّة بمقدار 50%. ستكون هذه المربّعات ضمن مخطّط مطلق Absolute Layout، وكلّ منها عبارة عن كائن ViewBox. سنعيّن مواقع وأبعاد هذه المربّعات باستخدام الواحدات المستقلة عن الجهاز independent-device units. أنشئ مشروعًا جديدًا من النوع Blank App (Xamarin.Forms Portable) وسمّه SimpleAbsoluteLayout، ثم أبق فقط على المشروعين SimpleAbsoluteLayout (Portable) و SimpleAbsoluteLayout.Droid كما وسبق أن فعلنا في هذا الدرس. بعد ذلك سنضيف صفحة محتوى تعتمد على رماز XAML كما وسبق أن فعلنا في هذا الدرس سنسمّيها SimpleAbsoluteLayoutPage. احرص على أن تكون محتويات هذه الصفحة على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="SimpleAbsoluteLayout.SimpleAbsoluteLayoutPage"> <AbsoluteLayout x:Name="absLayout"> <BoxView x:Name="boxAccent" Color="Accent" AbsoluteLayout.LayoutBounds="25, 100, 110, 110" Opacity="0.5"/> <BoxView x:Name="boxRed" Color="Red" AbsoluteLayout.LayoutBounds="50, 125, 110, 110" Opacity="0.5"/> <BoxView x:Name="boxGreen" Color="Green" AbsoluteLayout.LayoutBounds="75, 150, 110, 110" Opacity="0.5"/> </AbsoluteLayout> </ContentPage> محتويات هذه الصفحة بسيطة وواضحة. حيث نعمل على إنشاء مخطّط مطلق باستخدام الوسم . لاحظ كيف وضعنا ضمن هذا العنصر ثلاثة عناصر من النوع BoxView. وقد عيّنّا لكل عنصر منها اسمًا لأنّنا سنحتاجه في القسم الثاني من هذا الدرس. الملفت للنظر أنّ هذه العناصر قد تمّ وضعها مباشرةً ضمن العنصر AbsoluteLayout دون الحاجة إلى وجود الوسم وهذا جائز تمامًا. لاحظ بالنسبة إلى عنصر BoxView الأوّل: <BoxView x:Name="boxAccent" Color="Accent" AbsoluteLayout.LayoutBounds="25, 100, 110, 110" Opacity="0.5"/> الخاصيّة التي تتحكّم بموضعه هي AbsoluteLayout.LayoutBounds حيث أسندنا إليها النص: "25, 100, 110, 110" الذي يحتوي على أربعة أرقام تفصل بينها فواصل، وهي من اليسار إلى اليمين: بعد الزاوية اليسرى العليا من العنصر عن الحافة اليسرى للمخطّط المطلق ورمزه X (القيمة 25). بعد الزاوية اليسرى العليا من العنصر عن الحافة العليا للمخطّط المطلق ورمزه Y (القيمة 100). عرض العنصر ورمزه Width (القيمة 110). ارتفاع العنصر ورمزه Height (القيمة 110). انظر الشكل التخطيطي التالي لمزيد من التوضيح: في الحقيقة يمكنك اعتبار الشكل السابق كجملة إحداثية ديكارتيّة، ولكنّ محور التراتيب y-axis موجّه نحو الأسفل كما هو واضح بدلًا من الاتجاه المألوف نحو الأعلى. ينطبق نفس الأمر تمامًا على عنصريّ BoxView الباقيين: <BoxView x:Name="boxRed" Color="Red" AbsoluteLayout.LayoutBounds="50, 125, 110, 110" Opacity="0.5"/> <BoxView x:Name="boxGreen" Color="Green" AbsoluteLayout.LayoutBounds="75, 150, 110, 110" Opacity="0.5"/> وبما أنّ لكل من هذه العناصر الثلاثة نفس القيمة للعرض وللارتفاع، إذًا فهي عبارة عن مربّعات، وهي طبوقة فيما بينها. إذا نظرت إلى الأرقام الخاصّة بالموضع وبالحجم التي اخترتها "للمربّعات" الثلاث، فستلاحظ أنّني اخترتها بعناية كي تكون متداخلة فيما بينها. وقد جعلت الشفافيّة (الخاصية Opacity) لكلّ منها 50% (القيمة 0.5) لكي تعطي ذلك التأثير اللوني الجميل عندما تتداخل مع بعضها. انتقل الآن إلى الملف App.cs واحرص على ان تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new SimpleAbsoluteLayoutPage(); } نفّذ التطبيق باستخدام F5، لتحصل على شكل شبيه بما يلي: لنحرّك المربّعات! لنعمل على إضفاء القليل من الإثارة على التطبيق السابق. سنعمل على تحريك المربّعات السابقة بحركة رأسيّة تلقائيّة. أي أنّ المربّعات السابقة ستتحرّك نحو الأعلى حتى تصل إلى الحافة العليا للشاشة، ثم تعكس اتجاه الحركة نحو الأسفل حتى تصل إلى الحافة السفلى من الشاشة. ثم تعود من جديد لتعكس جهة الحركة لتصبح نحو الأعلى، وهكذا دواليك. في الحقيقة لن يطرأ أيّ تغيير على رماز XAML، فكل ما سنفعله هو إضافة الشيفرة البرمجيّة المناسبة لملف الشيفرة البرمجيّة الموافق لملف الرماز السابق. انتقل الآن إلى الملف SimpleAbsoluteLayoutPage.xaml.cs واحرص على أن تكون محتوياته على الشكل التالي: 1 using System; 2 using Xamarin.Forms; 3 4 namespace SimpleAbsoluteLayout 5 { 6 public partial class SimpleAbsoluteLayoutPage : ContentPage 7 { 8 private bool isUpDirection; 9 public SimpleAbsoluteLayoutPage() 10 { 11 InitializeComponent(); 12 13 isUpDirection = false; 14 15 Device.StartTimer(TimeSpan.FromMilliseconds(50), UpdatePositions); 16 } 17 18 private bool UpdatePositions() 19 { 20 Rectangle rect; 21 22 rect = AbsoluteLayout.GetLayoutBounds(boxAccent); 23 if (rect.Y - 10 <= 0) 24 { 25 isUpDirection = false; 26 } 27 28 rect = AbsoluteLayout.GetLayoutBounds(boxGreen); 29 if (rect.Y + rect.Height + 10 > this.Height) 30 { 31 isUpDirection = true; 32 } 33 34 BoxView[] boxViews = { boxAccent, boxRed, boxGreen }; 35 36 for (int i = 0; i < boxViews.Length; i++) 37 { 38 rect = AbsoluteLayout.GetLayoutBounds(boxViews[i]); 39 40 if (isUpDirection) 41 { 42 rect = new Rectangle(rect.X, rect.Y - 10, rect.Width, rect.Height); 43 } 44 else 45 { 46 rect = new Rectangle(rect.X, rect.Y + 10, rect.Width, rect.Height); 47 } 48 49 AbsoluteLayout.SetLayoutBounds(boxViews[i], rect); 50 } 51 52 return true; 53 } 54 } 55 } بالإضافة إلى بانية الصنف SimpleAbsoluteLayoutPage أنشأت تابعًا جديدًا أسميته UpdatePositions يمكنك أن تجده في الأسطر من 18 حتى 53. يُعتبر هذا التابع مسؤولًا عن تحريك المربّعات الثلاثة بالحركة الرأسيّة التي تحدثنا عنها قبل قليل. يتمّ استدعاء هذا التابع بشكل تلقائيّ كل 50 ميللي ثانيّة. التقنيّة التي استخدمناها في هذا الاستدعاء هي نفسها الموجودة في هذا الدرس. حيث استخدمنا التابع StartTimer من الصنف Device لهذا الغرض (انظر السطر 15). لاحظ الحقل isUpDirection من النوع bool المصرّح عنه في السطر 8. يُفيد هذا الحقل في الاحتفاظ بالاتجاه الحالي للمربّعات أثناء الحركة (أي هل الحركة نحو الأعلى أم نحو الأسفل). يبدأ التابع UpdatePositions بالتصريح عن المتغيّر rect من النوع Rectangle (السطر 20). و Rectangle هو عبارة عن بنية struct أي هو من نوع قيمة value type. يُفيد هذا النوع في وصف الأشكال المستطيلة عمومًا. فهو يمتلك أربع خصائص تصف حجم وموضع أيّ مستطيل على الشاشة. هذه الخصائص هي: X و Y و Width و Height وهي تصف على الترتيب: مقدار الإزاحة الأفقيّة عن الحد الأيسر، مقدار الإزاحة الرأسيّة عن الحد الأعلى، وعرض المستطيل، وارتفاع المستطيل. وذلك بمنطق مشابه لما ورد ذكره في الشكل الأوّل الذي تعرضنا له في هذا الدرس. نقوم في السطر 22 بالحصول على المستطيل (المربّع في واقع الأمر) الحاضن للعنصر boxAccent وهو المربّع الأوّل. ثمّ نختبر كون الإحداثي Y له قد أوشك أن يصبح سالبًا وذلك في السطر 23. فإن أوشك على ذلك فهذا يعني أنّ المربّع boxAccent قد ارتطم بالحد الأعلى للشاشة ويجب تغيير اتجاه الحركة ليصبح نحو الأسفل بدلًا من الأعلى، وهذا ما نفعله في السطر 25. نفس المنطق السابق نطبّقه على العنصر boxGreen وهو المربّع الثالث، وذلك في السطر 28. حيث نحصل على المستطيل الحاضن للعنصر boxGreen. ثمّ نختبر كون الحافة السفلى له (تأمل التعبير rect.Y + rect.Height + 10) قد أوشكت أن تتجاوز قيمتها ارتفاع الصفحة (وهو يماثل في مثالنا الارتفاع الكامل للمخطّط المطلق) وذلك في السطر 29. فإن أوشكت على ذلك فهذا يعني أنّ المربّع boxGreen سيتجاوز الحد السفلي للشاشة ويجب تغيير اتجاه الحركة ليصبح نحو الأعلى بدلًا من الأسفل، وهذا ما نفعله في السطر 31. بعد ذلك ننشئ المصفوفة boxViews بحيث تحتوي على عناصر المربّعات الثلاثة boxAccent و boxRed و boxGreen وذلك في السطر 34. ثمّ يدخل البرنامج حلقة for (الأسطر من 36 حتى 50) التي تنحصر وظيفتها في تحديث إحداثيات المربّعات الثلاثة بحيث تحرّكها نحو الأعلى أو نحو الأسفل بحسب قيمة الحقل isUpDirection. سيكون مقدار الانتقال الرأسي 10 وحدات في كل مرّة كما هو واضح في السطرين 42 و 46. ويتم إسناد الإحداثيّات الجديدة للمربّعات الثلاثة عن طريق استدعاء التابع الساكن AbsoluteLayout.SetLayoutBounds في السطر 49. حيث يتطلّب هذا التابع وسيطين. الأوّل هو المربّع المراد تطبيق الإحداثيات الجديدة عليه وهو موجود ضمن المصفوفة boxView، والثاني هو قيمة من النوع Rectange تحوي المستطيل (المربّع في مثالنا هذا) الحاضن الجديد. الخلاصة تحدثنا في هذا الدرس عن المخطّط المطلق Absolute Layout وكيفيّة استخدامه من خلال رماز XAML. يفيد هذا المخطّط كما أشرنا في التحكّم بحجم وتموضع أي عنصر مرئي على الشاشة على نحو دقيق. تناولنا تطبيقين بسيطين يوضّحان كيفيّة التحكّم بحجم وموضع كل عنصر من عناصر BoxView، كما استخدمنا التابع StartTimer من الصنف Device لتوليد استدعاءات تلقائيّة ذات أزمنة منتظمة إلى تابع مخصّص كي يتحكّم بمواضع هذه العناصر. سنتناول في الدرس القادم تطبيقًا عمليًّا هو في واقع الأمر تحديث للتطبيق الثاني في هذا الدرس لكي نتحكّم في مواضع العناصر بشكل مخصّص أكثر.