لوحة المتصدرين
المحتوى الأكثر حصولًا على سمعة جيدة
المحتوى الأعلى تقييمًا في 10/12/21 في كل الموقع
-
لقد طرحت سؤالاً متقدماً أكثر ويتعامل مع قواعد البيانات، فأعتقد أنك تستطيع حل هذه المشكلة.. const express = require('express') // تضمين المخدم const app = express() // بناء الغرض // تحديد المنفذ const port = 4000 // استقبال الطلبيات في المسار الجذر app.get('/', (req, res) => { // إرسال رد للمستخدم res.send('Hello World!') }) // تشغيل المخدم app.listen(port, () => { // ^^^^ تحديد المنذ من المتغير console.log(`Example app listening at http://localhost:${port}`) }) // التشغيل ضمن cmd node app.js2 نقاط
-
بالإضافة إلى إجابة الأستاذ بﻻل, فإنه يوجد عدد من الدوال الخاصة بالمصفوفات في الجافاسكريبت التي تُوضع تحت مُسمى البرمجة الوظيفية(functional programming) والتي تقوم بعدة وظائف تجعل الشفرة البرمجية أسهل في القراءة وأقصر, من بين تلك الدوال يوجد دالة تقوم بتنفيذ الغرض المطلوب من شفرتك البرمجية تُسمى بreduce , تلك الدالة وظيفتها أنها تقوم بالمرور على المصفوفة كلها وتستقبل متغير من نوع دالة الإرجاع(callback function) وتقوم بإعطاء دالة الإرجاع متغيرين عبارة عن العنصر السابق والعنصر الحالي في المصفوفة, وتقوم بتنفيذ عملية تُحددها أنت على تلك العناصر ويكون تعريف الدالة بالشكل التالي reduce((previousValue, currentValue) => { ... } ,initialValue) ويُمثل المتغير initialValue القيمة الإبتدائية للعنصر السابق, حيث في بداية المرور بالمصفوفة لا يكون هنالك عنصر سابق, فيمكن إذا تحويل الشفرة البرمجية خاصتك إلى كود أبسط كما في الشكل التالي const cart = [1,3,4,5,6]; const total = cart.reduce((previousValue, currentValue) => previousValue + currentValue,0); حيث تقوم الدالة بالمرور على عناصر المصفوفة, بدءاً من أول عنصر وهو "1" وتكون قيمة العنصر السابق الإبتدائية بصفر, فيتم جمع صفر مع 1 لتصبح قيمة العنصر الإبتدائية ب1 في الدورة التالية يتم جمع قيمة ال1 مع ال3 لتصبح 4 في الدورة التالية يتم جمع قيمة ال4 مع ال4 فتصبح 8 في الدورة التالية يتم جمع قيمة ال8 مع ال5 فتصبح 13 في الدورة التالية يتم جمع ال13 مع 6 فتصبح 19 ويمكنك قراءة هذا المقال حتى تفهم دوال المصفوفات بشكلٍ أفضل2 نقاط
-
أيهما أستخدم؟ وماذا على أن أغير في البرنامج حتى يعمل مع HTTPS هل أنا مضطر للتشفير (مثلا كلمات السر أو البيانات) حتى مع استخدام HTTPS هل من الضروري استعمال HTTPS؟1 نقطة
-
1 نقطة
-
ما هو الكود اللازم عند الضغط على button للايام يتم ظهور اشعار في برنامج اندرويد؟ مثلا اريد اظهار اشعار في يوم الاثنين؟1 نقطة
-
لدي الاستعلام من الشكل: SELECT * FROM Table1 LEFT JOIN Table2 ON Table1.id = Table2.user_id WHERE Table2.role='Admin' كيف أحصل على فلترة بشكل أسرع1 نقطة
-
حسب ترتيب خطوات تنفيذ الاستعلام في SQL سيتم تنفيذ جزء ON قبل جزء WHERE لذلك يمكننا نقل الشرط لتتم الفلترة بناءً عليه بخطوة قبل وهذا يسرع الأداء، حيث لا نحتاج WHERE بعدها SELECT * FROM Table1 LEFT JOIN Table2 ON Table1.id = Table2.user_id AND Table2.role='Admin' ^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^^^^^^^ لاحظ نقل شرط WHERE ودمجه مع شرط الربط. WHERE تعمل على الجدول الناتج من عملية الربط، أي سيكون هنالك بيانات أكثر لاختبارها، لذلك نضع الشرط مع جزء ON ضع دائمًا شروط الدمج في جملة ON إذا كنت تقوم بإجراء INNER JOIN. أي لا تضف أي شروط من WHERE إلى جملة ON ، بل ضعها في جملة WHERE إذا كنت تقوم بتنفيذ LEFT JOIN ، فقم بإضافة أي شروط WHERE إلى جملة ON للجدول الموجود في الجانب الأيمن من الدمج. هذا أمر لا بد منه ، لأن إضافة جملة WHERE تشير إلى الجانب الأيمن من الدمج ستحوله إلى INNER JOIN. الاستثناء هو عندما تبحث عن السجلات غير الموجودة في جدول معين. يمكنك إضافة المرجع إلى معرّف فريد (ليس فارغًا أبدًا) في جدول RIGHT JOIN إلى جملة WHERE بهذه الطريقة: WHERE t2.idfield IS NULL. لذا ، فإن المرة الوحيدة التي يجب أن تشير فيها إلى جدول على الجانب الأيمن من الصلة هي العثور على تلك السجلات غير الموجودة في الجدول.1 نقطة
-
كيف يقوم مخدم قواعد البيانات بتنفيذ الاستعلام؟ بأي جزء من الاستعلام يبدأ وأين ينتهي؟1 نقطة
-
لنفترض أنه لدينا استعلام فيه العديد من الكلمات المفتاحية سيكون ترتيب التنفيذ كالتالي: تحديد الجداول وخاصة الجدول اليساري FROM LEFT TABLE تحديد علاقة الربط ON التي تخص حقول الربط تحديد نمط الربط والجدول الثانوي (يميني - يساري - كامل LEFT - RIGHT - FULL ..) تطبيق الفلترة باستخدام الشرط الخاص بعبارة WHERE عملية التجميع التي تحددها GROUP BY عملية الفلترة على المجموعات الجزئية أي تطبيق شرط HAVING تحديد الأعمدة المراد جلب قيمها وهنا ينفذ جزء SELECT تحديد الفلترة الخاصة ب DISTINCT عمل الترتيب بواسطة ORDER BY تحديد عدد الحقول المعادة مثل LIMIT - OFFSET - TOP (خطوة 7) SELECT (خطوة 8) DISTINCT (خطوة 10) <top_specification> <select_list> (خطوة 1) FROM left_table (خطوة 3) join_type JOIN right_table (خطوة 2) ON join_condition (خطوة 4) WHERE where_condition (خطوة 5) GROUP BY group_by_list (خطوة 6) HAVING having_clause (خطوة 9) ORDER BY order_by_list1 نقطة
-
كيف يمكنني تحديد أحجام الخطوط لعنوان الشكل (figure) وتسميات المحور (labels). حيث أنني أريد أن تكون أحجامهم مختلفة: import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10, 1000) fig = plt.figure() plt.plot(x, np.sin(x), '--b', label ='Sine') plt.plot(x, np.cos(x), c ='r', label ='Cosine') fig.suptitle('test title') plt.xlabel('x-label') plt.ylabel('y-label') plt.show()1 نقطة
-
للقيام بذلك يمكنك استخدام الوسيط fontsize مع كل من هذه الدوال، أي يمكنك استخدام هذه الخاصية مع الدالة التي تحدد العنوان والدوال التي تحدد أسماء المحاور: import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10, 1000) fig = plt.figure() plt.plot(x, np.sin(x), '--b', label ='Sine') plt.plot(x, np.cos(x), c ='r', label ='Cosine') fig.suptitle('test title', fontsize=22) # تحديد حجم العنوان plt.xlabel('x-label', fontsize=15) # حجم تسمية المحور plt.ylabel('y-label', fontsize=15) # حجم تسمية المحور الثاني plt.show() الخرج: كما يمكنك القيام بتحديد ذلك بشكل globally بالشكل التالي: import matplotlib.pylab as pylab params = {'figure.figsize': (12, 6), # حجم الشكل 'axes.titlesize':30, # حجم عنوان الشكل 'axes.labelsize':20, # حجم اسم المحور الأفقي والعمودي 'ytick.labelsize': 16, # حجم علامات المحور العمودي 'xtick.labelsize':16} # .. الأفقي pylab.rcParams.update(params) import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10, 1000) plt.plot(x, np.sin(x), '--b', label ='Sine') plt.plot(x, np.cos(x), c ='r', label ='Cosine') plt.title('test title') plt.xlabel('xlabel') plt.ylabel('ylabel') plt.show() الخرج:1 نقطة
-
تحيه طيبه للجميع انا استعمل المكتبه التاليه : image_picker لختيار مجموعة من الصور في وقت واحد بستخدام : // Pick multiple images final List<XFile>? images = await _picker.pickMultiImage(); الكود يعمل بشكل ممتاز ولكن المشكله مثلا لو قمت باختيار 3 صور في اول مره بعدها رغبة باضافة المزيد لو قمت باضافة المزيد سوف يتم حذف الصور السابقه التي سبق وتم اختيارها ووضع الصور الجديده فقط مكانها كيف يمكن الاحتفاظ بصور السابقه في حالة اضافة صور جديده لها ياليت اذا احد واجه هذا المشكله ويعرف طريقة لحلها يفيدنا بارك الله فيكم الكود كامل: import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Image Picker Demo', home: MyHomePage(title: 'Image Picker Example'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { List<XFile> _imageFileList; set _imageFile(XFile value) { _imageFileList = value == null ? null : [value]; } dynamic _pickImageError; bool isVideo = false; final ImagePicker _picker = ImagePicker(); void _onImageButtonPressed(ImageSource source, {BuildContext context, bool isMultiImage = false}) async { if (isMultiImage) { try { final pickedFileList = await _picker.pickMultiImage( maxWidth: 66, maxHeight: 66, imageQuality: 66, ); setState(() { _imageFileList = pickedFileList; }); } catch (e) { setState(() { _pickImageError = e; }); } } } Widget _previewImages() { if (_imageFileList != null) { return Semantics( child: ListView.builder( key: UniqueKey(), itemBuilder: (context, index) { return Semantics( label: 'image_picker_example_picked_image', child: kIsWeb ? Image.network(_imageFileList[index].path) : Image.file(File(_imageFileList[index].path)), ); }, itemCount: _imageFileList.length, ), label: 'image_picker_example_picked_images'); } else if (_pickImageError != null) { return Text( 'Pick image error: $_pickImageError', textAlign: TextAlign.center, ); } else { return const Text( 'You have not yet picked an image.', textAlign: TextAlign.center, ); } } Future<void> retrieveLostData() async { final LostDataResponse response = await _picker.retrieveLostData(); if (response.isEmpty) { return; } if (response.file != null) { if (response.type == RetrieveType.video) { isVideo = true; } else { isVideo = false; setState(() { _imageFile = response.file; _imageFileList = response.files; }); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android ? FutureBuilder<void>( future: retrieveLostData(), builder: (BuildContext context, AsyncSnapshot<void> snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: case ConnectionState.waiting: return const Text( 'You have not yet picked an image.', textAlign: TextAlign.center, ); case ConnectionState.done: return _previewImages(); default: if (snapshot.hasError) { return Text( 'Pick image/video error: ${snapshot.error}}', textAlign: TextAlign.center, ); } else { return const Text( 'You have not yet picked an image.', textAlign: TextAlign.center, ); } } }, ) : _previewImages(), ), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { isVideo = false; _onImageButtonPressed( ImageSource.gallery, context: context, isMultiImage: true, ); }, heroTag: 'image1', tooltip: 'Pick Multiple Image from gallery', child: const Icon(Icons.photo_library), ), ), ], ), ); } }1 نقطة
-
تمام، الشرح: اختبار كل حالات القائمتين (فارغة أو غير فارغة + فارغة أو غير فارغة) عن طريق if else1 نقطة
-
الف شكر لك يا اخوي ربي يزيدك ويبارك لك علمك من واسع فضله الكود كامل بعد التعديل للفائده import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Image Picker Demo', home: MyHomePage(title: 'Image Picker Example'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { List<XFile> _imageFileList; set _imageFile(XFile value) { _imageFileList = value == null ? null : [value]; } dynamic _pickImageError; bool isVideo = false; final ImagePicker _picker = ImagePicker(); void _onImageButtonPressed(ImageSource source, {BuildContext context, bool isMultiImage = false}) async { if (isMultiImage) { try { final pickedFileList = await _picker.pickMultiImage( maxWidth: 66, maxHeight: 66, imageQuality: 66, ); if (_imageFileList == null) { if (pickedFileList == null) return; setState(() { _imageFileList = pickedFileList; }); } else { if (pickedFileList != null) { setState(() { _imageFileList = [..._imageFileList, ...pickedFileList].toSet().toList(); }); } // لا تغيير } } catch (e) { setState(() { _pickImageError = e; }); } } } Widget _previewImages() { if (_imageFileList != null) { return Semantics( child: ListView.builder( key: UniqueKey(), itemBuilder: (context, index) { return Semantics( label: 'image_picker_example_picked_image', child: kIsWeb ? Image.network(_imageFileList[index].path) : Image.file(File(_imageFileList[index].path)), ); }, itemCount: _imageFileList.length, ), label: 'image_picker_example_picked_images'); } else if (_pickImageError != null) { return Text( 'Pick image error: $_pickImageError', textAlign: TextAlign.center, ); } else { return const Text( 'You have not yet picked an image.', textAlign: TextAlign.center, ); } } Future<void> retrieveLostData() async { final LostDataResponse response = await _picker.retrieveLostData(); if (response.isEmpty) { return; } if (response.file != null) { if (response.type == RetrieveType.video) { isVideo = true; } else { isVideo = false; setState(() { _imageFile = response.file; _imageFileList = response.files; }); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android ? FutureBuilder<void>( future: retrieveLostData(), builder: (BuildContext context, AsyncSnapshot<void> snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: case ConnectionState.waiting: return const Text( 'You have not yet picked an image.', textAlign: TextAlign.center, ); case ConnectionState.done: return _previewImages(); default: if (snapshot.hasError) { return Text( 'Pick image/video error: ${snapshot.error}}', textAlign: TextAlign.center, ); } else { return const Text( 'You have not yet picked an image.', textAlign: TextAlign.center, ); } } }, ) : _previewImages(), ), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { isVideo = false; _onImageButtonPressed( ImageSource.gallery, context: context, isMultiImage: true, ); }, heroTag: 'image1', tooltip: 'Pick Multiple Image from gallery', child: const Icon(Icons.photo_library), ), ), ], ), ); } }1 نقطة
-
أريد وضع معلومات البيانات Legend خارج الرسم Plot: import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10, 1000) fig, ax = plt.subplots() ax.plot(x, np.sin(x), '--b', label ='Sine') ax.plot(x, np.cos(x), c ='r', label ='Cosine') ax.axis('equal') # هنا قمنا بتعريف صندوق لتوضيح البيانات leg = ax.legend(); # الصندوق سيكون في الزاويا العليا اليسارية وأيضاً كيف يمكنني تغيير حجم الخط فيه؟1 نقطة
-
للتحكم بذلك نستخدم الوسيطين loc و bbox_to_anchor: loc='best', bbox_to_anchor=(x, y) لذا بدايةً دعنا نتعرف على الوسيط loc الذي يأخذ القيم التالية: Location String 'best' 'upper right' 'upper left' 'lower left' 'lower right' 'right' 'center left' 'center right' 'lower center' 'upper center' 'center' فالقيمة "best" تعني أنه سيضعه في أفضل مكان ممكن تلقائياً وهي الحالة الافتراضية. أما "upper right" تعني أنه سيقوم بوضعه في أعلى اليمين وهكذا... أما الوسيط bbox_to_anchor فهو يستخدم لتغيير موضعه بالنسبة ل loc بحيث أن x و y هما إحداثيات الصندوق مثلاً القيمة (0.0, 0.0) تعني أنك تريد وضعه خارجاً عند مبدأ الإحداثيات. انظر للأمثلة التالية: مثال1: import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10, 1000) fig, ax = plt.subplots() ax.plot(x, np.sin(x), '--b', label ='Sine') ax.plot(x, np.cos(x), c ='r', label ='Cosine') ax.axis('equal') # هنا قمنا بتعريف صندوق لتوضيح البيانات #في أسفل اليمين legend سنضع ال leg = ax.legend(loc ="lower right"); الخرج: مثال2: هنا سنضعه في الخارج على اليمين (أنت تتحكم بالإحداثيات-عن طريق التجريب-). import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10, 1000) fig, ax = plt.subplots() ax.plot(x, np.sin(x), '--b', label ='Sine') ax.plot(x, np.cos(x), c ='r', label ='Cosine') ax.axis('equal') # هنا قمنا بتعريف صندوق لتوضيح البيانات # في أسفل اليمين legend سنضع ال # وسنقوم أيضاً بوضعه في الخارج leg = ax.legend(loc ="lower right",bbox_to_anchor =(1.3, 0.0)) الخرج: مثال3: سنضعه في الخارج عند مبدأ الإحداثيات: import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10, 1000) fig, ax = plt.subplots() ax.plot(x, np.sin(x), '--b', label ='Sine') ax.plot(x, np.cos(x), c ='r', label ='Cosine') ax.axis('equal') leg = ax.legend(bbox_to_anchor =(0.0, 0.0)) الخرج: أما بالنسبة لحجم الخط فنستخدم الوسيط fontsize حيث نمرر له حجم الخط مثلاً 11 أو نوع الخط الذي تحتاجه من خلال الكلاس FontProperties أو مباشرةً من خلال ذكر اسمه كما يلي: import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10, 1000) fig, ax = plt.subplots() ax.plot(x, np.sin(x), '--b', label ='Sine') ax.plot(x, np.cos(x), c ='r', label ='Cosine') ax.axis('equal') # لتعديل الخط from matplotlib.font_manager import FontProperties # نقوم بتعريف الخط الذي نحتاجه fontP = FontProperties() fontP.set_size('xx-small') # مثلاً هنا تريد أن يكون الخط صغيراً جداً # fontsize اللآن نمرر كائن الخط إلى الوسيط # leg = ax.legend(bbox_to_anchor =(0.6, -0.2), fontsize=fontP) # أو نقوم بتمرير نوع الخط مباشرةً كالتالي # leg = ax.legend(bbox_to_anchor =(0.6, -0.2), fontsize='xx-small') # أو من خلال ذكر الحجم leg = ax.legend(bbox_to_anchor =(0.7, -0.2), fontsize=20) الخرج:1 نقطة
-
لنحاول التعديل: try { final pickedFileList = await _picker.pickMultiImage( maxWidth: 66, maxHeight: 66, imageQuality: 66, ); if (_imageFileList == null) { if ( pickedFileList == null) return; setState(() { _imageFileList =pickedFileList; }); } else { if (pickedFileList != null) { setState(() { _imageFileList = [..._imageFileList, ...pickedFileList].toSet().toList(); }); } // لا تغيير } } catch (e) { setState(() { _pickImageError = e; }); }1 نقطة
-
أعتقد أن القائمة الأولى فارغة جرب التالي: setState(() { _imageFileList = [..._imageFileList, ...pickedFileList].toSet().toList(); }); ++ اختبر أن القائمة الجديدة ليست فارغة1 نقطة
-
1 نقطة
-
تريد الإضافة.. لربما اختار المستخدم نفس الصورة السابقة سوف تتكرر. سوف ندمج القائمة السابقة مع التحديد الجديد (القائمة الجديدة) بدون تكرار setState(() { _imageFileList = (_imageFileList + pickedFileList).toSet().toList(); }); الطريقة toSet تقوم بعمل مجموعة من القائمة، والمجموعة هي عناصر غير مكررة، سندمج القائمتين، ونعيد من الناتج مجموعة، ثم نحولها إلى قائمة.. الطريقة + تدمج سلسلتين في Dart 2، طريقة ثانية: final tmp = new List.from(_imageFileList)..addAll(pickedFileList); final tmp2 = tmp.toSet().toList(); setState(() { _imageFileList = tmp2; }); Dart الأقدم تستخدم addAll1 نقطة
-
@دانا دلول بدايتاً تطبيقات الأندرويد التي تقوم على بناء التذكيرات أو التنبيهات بخصوص أمر معين فليكن مثلاً تنبيه بخصوص موعد الدواء تعتمد برمجياً على المنبه و الإشعارات هذا بشكل ملخص ولا يوجد كود واحد لجميع تطبيق الأندرويد التي تعتمد على التنبيهات بل يمكن بناءها بعدة طرق لكن برمجياً بشكل عام يتم إستخدام AlarmManager و BroadcastReceiver لأن التنبيهات بالتأكيد ستكون عندما التطبيق يكون في الخلفية , الأن برمجياً سأضع مثال بسيط لما ذكرته في الأعلى لكن عدلي عليه بما يتناسب مع ما تحتاجين ,أولاً نقوم بعمل أوبجيكت من ال AlarmManager وأيضاً نقوم بعمل أوبجيكت من PendingIntent طبعاً بشكل ملخص AlarmManager reminder; PendingIntent reminderIntent; reminder = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(context, AlarmReceiver.class); reminderIntent = PendingIntent.getBroadcast(context, 0, intent, 0); reminder.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 60 * 1000, reminderIntent); //التنبيه للدقيقة القادمة ثم في ال AlarmReceiver كلاس التي قمنا بتمريرها في ال intent في الأعلى ,ما سنقوم به في الميثود onReceive الموجودة داخل كلاس AlarmReceiver نقوم بإرسال إشعار للمستخدم أو ما ترغبين به على حسب التطبيق الخاص بك public class AlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { //نقوم بإرسال إشعار من هنا أو نقوم بعمل ما نريد على حسب ما ترغبين } }1 نقطة
-
يمكنك معرفة أبعاد وحجم الصورة وأكبر قيمة فيها بالشكل التالي: import cv2 as cv # قراءة الصورة mat = cv2.imread('/content/closedeye1.jpg') # أبعادها height, width, channel = mat.shape[:3] # عدد عناصرها size = mat.size print("Height= ",height) print("Width= ",width) print("Size= ",size) maxElement = np.amax(mat) print('Max element from Numpy Array : ', maxElement) # الخرج """ Height= 186 Width= 271 Size= 151218 Max element from Numpy Array : 255 """1 نقطة
-
كما تعلم فإن مرشحات الحدَ هدفها الأساسي هو العمل المعاكس لمرشحات التنعيم، حيث أن مرشحات التنعيم تقوم بعمل تشويش للصورة وأهم تطبيقاتها هي إزالة الضجيج أو تنعيم الصورة، أما مرشحات الحدَ، فهي بالعكس، حيث أنها تقوم بتوضيح الحواف. كما أنه كنصيحة مني فإنه في حالة كانت الصورة الخاصة بك تحوي بعض الضجيج فلابد من أن تقوم أولاً بتطبيق مرشح تنعيم على الصورة لإزالة الضجيج منها، فالضجيج هو حافة في نهاية الأمر وبالتالي تطبيق مرشح تعزيز (حدَ) سيؤدي إلى تعزيز الضجيج وهذا أمر غير مرغوب فيه. هناك تقنيات عدة تقنيات لزيادة حدة الصورة وتعتمد كلها على تقنيات الحد sharpening Spatial Filters، وتعتمد على نوعين من المشتقات، مشتقات الدرجة الأولى مثل مرشح سوبل أو الثانية مثل مرشح لابلاسيان، ثم نستخدم المعادلة التالية -مثلاً في حالة استخدام لابلاسيان تكون: g(x,y)=f(x,y)+c[∇2f(x,y)] g(x,y): الصورة المعززة f(x,y): الصورة الأصلية ∇2f(x,y): الصورة بعد تطبيق مرشح الحد(لابلاسيان) حيث أن قيمة المعامل c هي إما 1 أو -1 وتعتمد على الكيرنل التي استخدمتها في تطبيق لابلاسيان وتساوي إما 1 أو -1 بحيث: laplacian1=np.array( [[0,1,0], [1,-4,1], [0,1,0]]) # تكون قيمة c=1- laplacian2=np.array( [[1,1,1], [1,-8,1], [1,1,1]]) # c=1- laplacian3=np.array([[0,-1,0], [-1,4,-1], [0,-1,0]]) # c=+1 laplacian4=np.array([[-1,-1,-1], [-1,+8,-1], [-1,-1,-1]]) # c=+1 في المثال التالي سنقوم بتطبيق الفكرة السابقة وهي أفضل طريقة لزيادة حدة الصورة بالاعتماد على مشتقات الدرجة الثانية (لابلاسيان) والمعادلة السابقة: import cv2 as cv import matplotlib.pyplot as plt # قراءة الصورة image = cv2.imread('/content/closedeye1.jpg',1) # matplotlib هنا فقط سأقوم بتحويل فضاء الألوان لأنني سأقوم بعرض الصورة باستخدام image=cv2.cvtColor(image, cv2.COLOR_BGR2RGB) plt.imshow(image) # سنجرب فلاتر لابلاسيان المختلفة laplacian1=np.array([[0,1,0],[1,-4,1],[0,1,0]]) # c=-1 laplacian2=np.array([[1,1,1],[1,-8,1],[1,1,1]]) # c=-1 laplacian3=np.array([[0,-1,0],[-1,4,-1],[0,-1,0]]) # c=1 laplacian4=np.array([[-1,-1,-1],[-1,8,-1],[-1,-1,-1]]) # c=1 # تطبيق الفلاتر لنرى نتيجة كل واحد على الصورة image_laplacian1=cv.filter2D(src=image, kernel=laplacian1, ddepth=-1) image_laplacian2=cv.filter2D(src=image, kernel=laplacian2, ddepth=-1) image_laplacian3=cv.filter2D(src=image, kernel=laplacian3, ddepth=-1) image_laplacian4=cv.filter2D(src=image, kernel=laplacian4, ddepth=-1) # تعزيز الصور image_sharping_1=cv2.subtract(image,image_laplacian1) image_sharping_2=cv2.subtract(image,image_laplacian2) image_sharping_3=cv2.add(image,image_laplacian3) image_sharping_4=cv2.add(image,image_laplacian4) # عرض النتائج import matplotlib.pyplot as plt plt.figure(figsize=(15,30)) plt.subplot(521) plt.imshow(image) plt.title("original image") plt.subplot(522) plt.imshow(image_sharping_1) plt.title("sharpenend_image 1") plt.subplot(523) plt.imshow(image_sharping_2) plt.title("sharpenend_image 2") plt.subplot(524) plt.imshow(image_sharping_3) plt.title("sharpenend_image 3") plt.subplot(525) plt.imshow(image_sharping_4) plt.title("sharpenend_image 4") الخرج:1 نقطة
-
اريد انشاء سيرفر باستخدام Node.js, and express.js والسيرفر يرن على البورت 4000 ويرد على الكلاينت بالمسج التالية : Thank you for your request1 نقطة
-
استبدل النص المرسل للمستخدم في دالة send: res.send('Hello World!') ^^^^^^^^^^^^ // Thank you for your request res.send('Thank you for your request')1 نقطة
-
طيب كيف ارد على الكلاينت بالمسج التالية : Thank you for your request1 نقطة
-
massage = input("> ") words = massage.split(" ") emojes = { ":(" : "😞", ":)" : "😄" } output = "" for word in words : output += emojes.get(word , word) + " " print(output) لم افهم الكود احتاج الى شرح و شكرا1 نقطة
-
1 نقطة
-
1 نقطة
-
1 نقطة
-
تحياتي هل يوجد واجبات او اختبارات قصيره بعد كل درس؟ وهل يوجد خانه او مكان استطيع تطبيق ما تم دراسته؟1 نقطة
-
يمكنك تطبيق ما تتعمله مع المدرب و المتابعة مع المدرب بشكلٍ جيد و بحرص, التطبيق سيكون عملي بحيث ستقوم بتنزيل جميع البرامج التي يستعملها المدرب و من ثم تقوم بالتطبيق معه, أما بخصوص الواجبات فهذا الأمر يرجع لك بحيث بعد الأنتهاء من الدروس يمكنك المحاولة في كتابة الأكواد بنفسك و تطبيق ما تعلمته بشكلٍ فردي و تعمل على مقارنة الأخطاء و العمل على إصلاحها و حلها بنفسك .1 نقطة
-
المقصود هنا تبسيط لفكرة المتغير, ما تريد أن توصله الأستاذة رضوى, أن rate ليس رقم أو عدد, وإنما متغير قد يحمل أي قيمة في أي لحظة , فالأصح أن ﻻ نقول إننا ضربنا الinterest في 0.07, بل أن نقول أننا ضربنا الinterest في قيمة الrate والتي كانت في لحظة الضرب تحمل القيمة 0.07, الموضوع أشبه بحملك لحقيبة تحتوي على مﻻبس ويسألك شخصاً ما عن ما بيدك فتُخبره أنها حقيبة, من الممكن أن تخبره أنها حقيبة تحتوي على مﻻبس, ولكن ليس من المﻻئم إخباره أن ما بيدك "مﻻبس" وإنما هي حقيبة تحتوي على المﻻبس1 نقطة
-
كيفة عمل اشعار ما هو الكود اللازم عند الضغط على button يتم ظهور بعد ثلاث ساعات مثلا اشعار في برنامج اندرويد؟؟ يعني اريد اظهار رسالة التنبيه بعد ثلاث ساعات من الضغط على button1 نقطة
-
1 نقطة
-
نعم يمكنك إن لم يكن لديكي انت مانع، فالملف يخصك. هل لاحقة الملف pdf.؟1 نقطة
-
الأمر prompt ينتمي للغرض window الذي يوجد في بيئة المتصفح، NodeJS لا يحوي مثل الغرض window وبالتالي هذا التابع، الحل يكون بانشاء ملف HTML وربط ملف الـ JavaScript الذي لديك به سيمكنك ذلك من اختبار الشيفرة داخل بيئة المتصفح محتوى ملف الصفحة سيكون كالتالي <html> <head> <script src="ali.js"></script> </head> <body> </body> </html>1 نقطة
-
ملاحظات: عندما نريد 16 بت للعدد نضع أصغر افي الجزء الأيسر للتمثيل الثنائي للعدد لنضمن عدد الخانات عملية تحويل العدد للنظام الثانئي تتمثل باستخراج باقي القسمة لهذا العدد على العدد 2 وتكرار الخطوة لناتج القسمة على 2 لنفس العدد المتمم الأحادي هو مقلوب تمثيل العدد الثنائي أي نبدل كل 0 ب 1 والعكس 1 ب 0 المتمم الثاني هو 1 + المتمم الأحادي تحويل عدد عشري لثائي: بداية الحلقة نأخذ باق القسمة على 2 للعدد ونضع القيم من اليمين لليسار نقسم العدد على 2 نكرر الحلقة مثال لتحويل الرقم 64 من عشري لثنائي من 16 خانة: 64 = 0000000001000100 المتمم الأحادي، كل بت نقلب قيمته: 64 = 0000000001000100 64 متمم = 1111111110111011 المتمم الثنائي ينتج عن المتمم الأحادي بجمع 1 64 متمم أولي = 1111111110111011 + 0000000000000001 64 متمم ثانوي = 1111111110111111 حاولي نفس الخطوات1 نقطة
-
يمكنك استخدام plt.tick_params التي تمكننا من التحكم بأمور مثل تغيير مظهر العلامات ticks، وعلامات التجزئة tick labels، وخطوط الشبكة gridlines. ولها الشكل التالي: matplotlib.pyplot.tick_params( axis='both',bottom=True, top=True, left=True, right=True,labelbottom=True, labeltop=True, labelleft=True, labelright=True,direction="out", length=None,width =None,color=None,which="major" labelsize=None,labelcolor=None,pad=None ) حيث أن axis تمثل المحاور التي نريد تطبيق التغيرات عليها ويأخذ القيم {'x', 'y', 'both'}. والوسطاء bottom, top, left, right يحددون فيما إذا كانت العلامات ticks ستظهر أم لا في الاتجاهات المحدد ويأخذون قيمة منطقية إما True لإظهارها أو False لإخفائها مثلاً bottom=True تعني أنك تريد إخفاء الticks من المحور السيني . بالنسبة ل labelbottom, labeltop, labelleft, labelright فهم يحددون فيما إذا كنت تريد عرض الملصقات التي تكون بجانب ال ticks (الأرقام) ويأخذون قيمة منطقية إما True لإظهارها أو False لإخفائها. أما direction فهو يحدد الاتجاه الذي سيتم وضع العلامات فيه {'in', 'out', 'inout'}. أما length فلتحديد طول العلامات و width لعرضها (قيم float). أما color فلتحديد لونها (مثلاً "r" أي أحمر). أما labelcolor و labelsize لتحديد لون وحجم الملصقات. أما which فكما نعلم قد يكون هناك ticks أساسية وثانوية فهنا من خلال هذا الوسيط أنت تحدد فيما إذا كنت تريد تطبيق التغيرات على العلامات الريسية أم الثانوية أم كلاهما {'major', 'minor', 'both'}. و pad لتحديد مقدار المسافة الفاصلة بين العلامات والملصقات. إليك المثال التالي لحذف العلامات من المحور السيني: import matplotlib.pyplot as plt import numpy as np x = np.array([0, 1, 2, 3]) y = np.array([3, 8, 1, 10]) plt.subplot(1, 2, 1) plt.title("First Plot") plt.plot(x,y) plt.tick_params( axis='x', # تطبيق التغيرات على المحور السيني فقط which='both', # تطبيق التغيرات على العللامات الرئيسة كونه لاتوجد علامات فرعية bottom=0, # إزالة العلامات من المحور السيني top=0, # .... labelbottom=0) # إخفاء الأرقام plt.show() مثال آخر بشكل عام لتوضيح عمل الوسيط which وباقي الوسطاء حيث سنقوم بإضافة علامات رئيسية وفرعية وسنقوم بالتعديل عليها (يمكنك أن تجرب باقي الوسطاء وتتلاعب بقيمها): from matplotlib.ticker import MultipleLocator, ScalarFormatter import matplotlib.pyplot as plt fig, ax = plt.subplots() ax.plot([0, 10, 20, 30], [0, 2, 1, 2]) ax.xaxis.set_minor_locator(MultipleLocator(1)) ax.xaxis.set_minor_formatter(ScalarFormatter()) ax.tick_params(axis ='both', which ='major', labelsize = 16, pad = 102, colors ='r') ax.tick_params(axis ='both', which ='minor', labelsize = 8, colors ='b') plt.show() كذلك هناك الوسيط grid_color للتحكم بلون خطوط الشبكة (إن وجدت) و grid_alpha للتحكم بشفافيتها و grid_linewidth للتحكم بعرضها (قيم float).1 نقطة
-
لتغيير الفضاء اللوني نستخدم التابع cvtColor الذي له الشكل التالي: cvtColor(image, code) حيث أن الوسيط الأول يمثل الصورة التي نريد تطبيق التحويل عليها، أما الوسيط الثاني يمثل كود تحويل (أكواد جاهزة في المكتبة فقط نقوم بتحديد اسم كود التحويل). وبالتالي للتحويل من صورة ملونة BGR (أغلب الصور تنتمي لهذا الفضاء اللوني) إلى صورة ملونة HSV نستخدم الكود cv2.COLOR_BGR2HSV كالتالي: import cv2 import numpy as np # قراءة الصورة img1 = cv2.imread(r'C:\Users\Windows.10\Desktop\Safedrive\me.jpg') # HSV تحويل الصورة إلى imageresult = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV) # عرض الصورة الناتجة وتخزينها cv2.imshow('res',img1) cv2.waitKey(0) cv2.destroyAllWindows() cv2.imwrite('d:\HSV.png',imageresult) وللتحويل من صورة ملونة BGR إلى صورة رمادية Gray نستخدم cv2.COLOR_BGR2GRAY بنفس الطريقة السابقة. وفي الرابط التالي من التوثيق الرسمي ل OpenCV جدول بحوالي 150 كود تحويل لكافة الأنظمة اللونية هنا.1 نقطة
-
بداية إن لم يكن لديك فكرة عن العمليات على مستوى البت، فأنصحك بالمقالة التالية أكاديمية خسوب. هذا التابع له الشكل التالي bitwise_and: bitwise_and(image1, image2, destination_array, mask) الوسيط الأول يمثل الصورة الأولى والوسيط الثاني يمثل الصورة الثانية أما الوسيط الثالث سيمثل المصفوفة التي سيتم وضع الخرج فيها، والوسيط الرابع يمثل القناع. ولهذا التابع فوائد واستخدامات كثيرة جداً على سبيل المثال سأقوم من خلال الكود التالي بتطبيق لوغو (شعار) على صورة. لكن قبل ذلك سنوضح الوسطاء بشكل أكبر: أولاً: الصورتين image1 و image2 كل منهما يتم تمثيله كمصفوفة ثلاثية الأبعاد من الشكل (height,width,channels) والصورتان يجب أن يكون لهما نفس الأبعاد بالضبط. حيث سيتم تنفيذ عملية bitwise_and بين بكسلات الصورة الأولى والثانية (سيتم دمج الصورتين). ثانياً: القناع mask يمكن أن تضعه ويمكن أن لاتضعه حسب المهمة التي تعمل عليها، وهذا القناع يكون من بعدين فقط أي (height,width) فقط (بدون قنوات) وأيضاً يجب أن يكون حجمه مطابق لحجم الصورتين. وتكون قيمه ضمن المجال 0 ل 255. هذا القناع يستخدم لتحديد الأماكن التي سيتم تطبيق عملية bitwise_and عليها والأماكن (البكسلات) التي لن يتم تطبيق عملية bitwise_and عليها بحيث مثلاً من أجل البكسل (x,y) من الصورتين image1 و image2 يتم النظر إلى القيمة الموجودة في ال mask فإذا كانت 0 لايتم تطبيق عملية bitwise_and وإلا فسيتم تطبيق عملية bitwise_and بين البكسلين. الآن سأقوم بكتابة الكود السابق لتفهم بدقة: import cv2 import numpy as np # قراءة الصورتين img1 = cv2.imread(r'C:\Users\Windows.10\Desktop\Safedrive\me.jpg') # الصورة img2 = cv2.imread(r'C:\Users\Windows.10\Desktop\Safedrive\car.webp') # اللوغو # نريد وضع اللوغو في الزاوية العليا اليسارية من الصورة img2=cv2.resize(img2,(180,140)) # سنقوم بتحديد حجم اللوغو بالشكل الذي يناسبنا # نقوم الآن بتحديد أبعاد اللوغو rows,cols,channels = img2.shape # نقوم باقتصاص الزاويا العليا اليسارية من اللوغو بحيث يكون الحجم مطابق لحجم اللوغو roi = img1[0:rows, 0:cols ] #هي المنطقة التي نريد وضع اللوغو فيها roi إذاً # نقوم الآن باستخلاص نسخة من الصورة تكون رمادية img2gray = cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY) # img2gray ستمثل النسخة الرمادية من الصورة # النسخة الرمادية: # نقوم الآن بتعتيب الصورة بحيث نحصل على صورة أبيض وأسود # البكسلات التي تكون قيمتها أكبر من 60 ستصبح 255 أي بيضاء # البكسلات الأقل ستصبح 0 أي سوداء _, mask = cv2.threshold(img2gray, 60, 255, cv2.THRESH_BINARY) # mask ستمثل نسخة الأبيض والأسود من الصورة # mask وهذا هو شكل صورة ال : # نقوم بعكس القيم # نعلم أن 255 تكافئ 1111 1111 في النظام الثنائي # ونعلم أن 0 تكافئ 0000 0000 في النظام الثنائي كوننا نمثل البكسل ب8 بت # ستؤدي إلى جعل ال 0 تساوي 255 والعكس bitwise_not وبالتالي تنفيذ عملية # أي الأبيض أسود والأسود أبيض mask_inv = cv2.bitwise_not(mask) # mask_inv سيجعل الخلفية بيضاء أي 255 والشعار 0 أي أسود # mask_inv: #على ال bitwise_and كقناع لتطبيق عملية mask_inv سنقوم باستخدام #وبالتالي المناطق التي تحوي 0 لن تدخل في عملية الدمج roi roi = cv2.bitwise_and(roi,roi,mask = mask_inv) # اي هنا سيتم تعتيم المنطقة التي نريد وضع اللوغو فيها # الآن نكون انتهينا من تحديد المكان الذي سيتم وضع اللوغو ضمنه # roi: # الآن نقوم ياستخلاص المنطقة التي يكون فيها اللوغو فقط من صورة اللوغو وباقي المناطق يتم تعتيمها أي 0 logo = cv2.bitwise_and(img2,img2,mask = mask) # logo: # roi الآن نضع اللوغو ضمن ال # ثم نقوم بتعديل الصورة الأصلية # تذكر أن : black+anycolor=anycolor merge = cv2.add(roi,logo) #roi قمنا بإضافة اللوغو الآن إلى ال img1[0:rows, 0:cols ] = merge cv2.imshow('res',img1) cv2.waitKey(0) cv2.destroyAllWindows()1 نقطة
-
توفر مكتبة matplotlib الأداة savefig لتخزين الرسم البياني الناتج: savefig(fname, dpi=None, format=None,bbox_inches=None) حيث أن الوسيط الأول يمثل المسار الذي تود فيه حفظ الملف الناتج، وال dpi أو dots per inch (وهي وحدة قياس تحدد عدد النقاط الفردية التي يمكن وضعها في مربع 1 × 1 بوصة) وزيادتها تؤدي لزيادة حجم الصورة. أما الوسيط الثالث فهو لتحديد الصيغة التي سيتم فيها حفظ الشكل البياني أي 'png', 'pdf', 'svg' ...إلخ. أما الوسيط الأخير ففي حال ضبطه على tight سيتم إزالة المسافة البيضاء الغير مرغوب فيها التي تكون حول الرسم البياني. هنا سأقوم بإنشاء رسم بياني وحفظه: import matplotlib.pyplot as plt fig, ax = plt.subplots( nrows=1, ncols=1 ) #figure إنشاء ax.plot([0,1,2], [10,20,3]) fig.savefig('to.png') # حفظه plt.close(fig) # figure إغلاق ال وبالنسبة للمثال الخاص بك: import pylab as pt pt.figure(1, figsize=(6, 6)) nationalism = 'European','Asian', 'Indian' ratio = [15, 35, 55] ax = pt.axes([0.1, 0.1, 0.8, 0.8]) pt.pie(ratio, explode=(0, 0.05, 0), labels=nationalism, shadow=True) pt.savefig('al.png') # حفظه كما يجب أن تتجنب استخدام التعليمة imshow لعرض الشكل قبل حفظه وإلا فستكون الصورة التي سيتم تخزينها صورة فارغة.1 نقطة
-
يمكنك ذلك من خلال جمع الصورتين مع إعطاء وزن لكل صورة (لتحديد الشفافية) مع إضافة تصحيح غاما (اختياري) وتوفر مكتبة OpenCV التابع addWeighted لإتمام المهمة: img = cv2.addWeighted(source1, alpha, source2, beta, gamma) حيث أن الوسيط الأول يمثل الصورة الأولى والوسيط الثاني alpha يمثل وزنها (مقدار الشفافية) والوسيط الثالث هو الصورة الثانية والرابع وزنها beta، حيث سيتم ضرب كل بكسل بالصورة الأولى ب alpha وكل بكسل في الثانية بقيمة beta، أما غاما فهي قيمة ستضاف للناتج (هنا هي لضبط شدة السطوع brightness أو التباين contrast في ألوان الصورة بغاية زيادة جودتها). المعادلة التالية تعبر عن العملية حيث تعتمد قيمة كل بكسل من بكسلات الخرج التي ستمثل الصورة الممزوجة على المعادلة التالية: G(x)= alpha*f0(x)+(1 – alpha)*f1(x) + gamma # أو بشكل أبسط Image= alpha * image1 + beta * image2 + y +gamma نلاحظ أنه من خلال تغيير قيمة ألفا، والتي ستتراوح من 0 إلى 1، يمكننا بسهولة التحويل من صورة إلى أخرى، وتتراوح قيمة هذه الأوزان من 0 إلى 1، ومن ثم يمكننا الحصول على العرض المطلوب للصور حسب حاجتنا. وبالنسبة لغاما يمكنك ضبطها على 0 لكي تتجاهل تأثيرها. والمثال التالي سيوضح ذلك، حيث سنقوم بدمج الصورتين التاليتين: والكود اللازم: import cv2 as cv # نقوم بقراءة الصور img1 = cv.imread(r'C:\Users\Windows.10\Desktop\Safedrive\closedeye1.jpg',1) img2 = cv.imread(r'C:\Users\Windows.10\Desktop\Safedrive\car.jpg',1) # عرض أبعاد الصورة الثانية img2.shape # (194, 259, 3) # أبعاد الصورة الأولى img1.shape # (186, 271, 3) #resize يجب أن تكون أبعاد الصور متاطابقة لذا نوحد حجمهما من خلال التابع img2 = cv.resize(img2,(190,250)) img1 = cv.resize(img1,(190,250)) # أو كان بالإمكان جعل حجم الصورة الثانية مطابقة للأولى كالتالي # img2 = cv2.resize(img2,(img1.shape[1],img1.shape[0])) # لكن الطريقة الأولى أفضل في حال كانت الصورتين بأبعاد مختلفة وللتحكم بذلك بشكل أدق # اللآن سنقوم بدمج الصورتين img = cv.addWeighted(img1,0.6,img2,0.4,0) cv.imshow('image',img) cv.waitKey(0) cv.destroyAllWindows() cv2.destroyAllWindows() الخرج:1 نقطة
-
لقراءة الفيديو يمكنك استخدام الكود التالي: import numpy as np import cv2 as cv #نحدد مسار الفيديو الذي نريد التقاطه captured = cv.VideoCapture(r'C:\Users\Windows.10\Downloads\Video\videoplayback_6.mp4') while(True): # التقاط الفيديو إطار تلو الآخر ret, frame = captured.read() # هنا يمكنك القيام بالعمليات التي تريدها على إطارات الفيديو # على سبيل المثال تريد عرض الفيديو بالصيغة الرمادية: gray_frame = cv.cvtColor(frame, cv2.COLOR_BGR2GRAY) # سيتم تحويل كل إطار ملون إلى إطار رمادي # عرض الإطار cv2.imshow('frame',gray_frame) #من لوحة المفاتيح a الانتظار 25 ميلي ثانية قبل قراءة الإطار التالي وفي هذه الأثناء إذا ضغطت على الزر # سيتم الخروج من الفيديو if cv2.waitKey(25) == ord('a'): break # عند الانتهاء من الفيديو يتم تدمير النافذة cv2.destroyAllWindows() حيث أن التابع VideoCapture هو التابع الذي يمكننا من التقاط الفيديو وهنا كوننا نقوم بقراءة فيديو من القرص يجب أن نقوم بتمرير مسار الصورة، أما لو كنا نريد التقاط الفيديو من كاميرا فيجب أن نمرر له رقم يمثل ترتيب الكاميرا وبالتالي إذا كنت تستخدم كاميرا وحيدة فنمرر له 0 أي الكاميرا الأولى والوحيدة: cv.VideoCapture(0) أما إذا كان لديك عدد أكبر -5 كاميرات مثلاً- وأردت التقاط الفيديو من الكاميرا الثالثة، نمرر له الرقم 2 وهكذا.. وكما نعلم فإن الفيديو هو عبارة عن إطارات (صور) يتم عرضها بسرعة عالية جداً وبالتالي سنستخدم حلقة while لقراءة هذه الاطارات الواحد تلو الآخر حيث نقوم ضمن الحلقة while بقراءة الصورة من خلال التابع read الذي يعيد قيميتين الأولى هي ret وهي قيمة منطقية (True|False) بحيث إذا قرأ إطار يكون True وإذا لم يقرأ إطار يكون False (أي انتهى أو هناك خطأ ما). والآن بعد قراءة الإطار يمكنك القيام بأي نوع تريده من العمليات على سبيل المثال قمنا بتحويل الإطار إلى الصيغة الرمادية من خلال التابع cvtColor ثم بعدها قمنا بعرض الإطار ثم قمنا باستخدام التابع waitKey للانتظار مدة زمنية محددة -25 ميلي ثانية- قبل الدخول في تكرار جديد للحلقة while وقراءة إطار جديد. أيضاً خلال هذه المدة -25 ميلي ثانية- إذا ضغطنا على الزر "a" من لوحة المفاتيح سيتم إيقاف الفيديو (إغلاق النافذة) وإنهاءه. ^-^هذا كل شيء ^-^1 نقطة
-
WebSocket هو بروتوكول الاتصال الذي يوفر اتصالاً ثنائي الاتجاه بين العميل والخادم عبر TCP. يظل WebSocket مفتوحاً طوال الوقت، لذا فهو يسمح بنقل البيانات في الوقت الفعلي (real time). عندما يقوم العملاء بإرسال الطلب إلى الخادم، فإنه لا يغلق الاتصال عند تلقي الاستجابة، بل يستمر وينتظر العميل أو الخادم لإنهاء الطلب. ميزاته: يساعد WebSocket في الاتصال في الوقت الفعلي بين العميل وخادم الويب. يساعد هذا البروتوكول في التحول إلى cross-platform في عالم ال real-time بين الخادم والعميل. يتيح أيضاً للأعمال التجارية في جميع أنحاء العالم الحصول على تطبيق ويب في الوقت الفعلي لتعزيز وزيادة الجدوى. الميزة الرئيسية أنه يعتمد على اتصال HTTP حيث أنه يوفر اتصال مزدوج الاتجاه. إليك مخطط له: لماذا نحتاجه: يوفر اتصالًا ثنائي الاتجاه ، مما يساعد في استمرار الاتصال الذي تم إنشاؤه بين العميل وخادم الويب. كما أنه يفي بالمعايير ويوفر الدقة والكفاءة في أحداث التدفق (من وإلى) مع زمن انتقال ضئيل negligible latency. WebSocket يزيل الحمل ويقلل من التعقيد. يجعل الاتصال في الوقت الفعلي سهل وفعال. Socket.IO هي مكتبة تتيح الاتصال في الوقت الفعلي مع نمط اتصال full-duplex بين العميل وخوادم الويب. ويستخدم بروتوكول WebSocket لتوفير الواجهة. وكلاً من WebSocket vs Socket.io هما مكتبات تعتمد على الأحداث (مقادة بال event). من جانب العميل: هي المكتبة التي تعمل داخل المتصفح. من جانب الخادم: مكتبة Node.js. ميزاته: يساعد في البث إلى مآخذ متعددة في وقت واحد ويتعامل مع الاتصال بشفافية. يعمل على جميع المنصات أو الخادم أو الجهاز ، مما يضمن المساواة والموثوقية والسرعة. يقوم تلقائياً بترقية المتطلبات إلى WebSocket إذا لزم الأمر. هو بروتوكول نقل مخصص في الوقت الفعلي. تنفيذه يكون فوق البروتوكولات الأخرى يتطلب استخدام كلا المكتبتين من جانب العميل بالإضافة إلى مكتبة من جانب الخادم. يعمل IO على الأحداث القائمة على العمل "work-based events". هناك بعض الأحداث المحجوزة التي يمكن الوصول إليها باستخدام Socket على جانب الخادم مثل Connect و message و Disconnect و Ping و Reconnect. هناك بعض الأحداث المحجوزة القائمة على العميل مثل الاتصال وخطأ الاتصال ومهلة الاتصال وإعادة الاتصال وما إلى ذلك. لماذا نحتاجه: يتعامل مع مستوى الدعم المتنوع "various support level" والتناقضات "inconsistencies" من المتصفح. تتعامل مع التدهورات الناتجة من استخدام البدائل التقنية لتوفر اتصال full-duplex في الوقت الفعلي. بعض المقارنة بينهم: WebSocket هو البروتوكول الذي تم إنشاؤه عبر اتصال TCP. بينما Socket هي مكتبة تعمل مع WebSocket. WebSocket يوفر الاتصال عبر TCP ، بينما Socket.io هي مكتبة لتجريد اتصالات WebSocket. لا يحتوي WebSocket على خيارات fallback، بينما يدعمها Socket.io. WebSocket هي التقنية ، بينما Socket.io هي مكتبة لـ WebSocket. WebSocket لايدعم ال broadcasting بينما Socket يدعمها. Proxy و load balancer غير مدعومين في WebSocket. بينما مدعومين في socket. Socket يوفر الاتصال القائم على الحدث بين المتصفح والخادم. بينما WebSocket يوفر اتصال مزدوج الاتجاه لاتصالات TCP.1 نقطة
-
قد تَكُون البرمجة (programming) صعبة نوعًا ما، وذلك كغَيْرها من الأنشطة المُفيدة والجديرة بالاهتمام، ومع ذلك، فهي عادة ما تَكُون مُجْزيّة ومُمتعة. عند كتابة برنامج (program)، لابُدّ أن تُخبِر الحاسوب بكُل تَفصيلة صغيرة يَنبغي له تَّنْفيذها، وبصورة صحيحة تمامًا؛ وذلك لأنه سيَتَّبِع البرنامج كما هو مَكتوب تمامًا وبطريقة عمياء. إذن، كيف تُكتَب البرامج غَيْر البسيطة؟ في الواقع، إنه ليس أمرًا غامضًا، فالأمر بِرُمَّته يَعتمِد فقط على تَعَلُّم طريقة التفكير الصحيحة. يُعدّ البرنامج (program) تعبيرًا عن فكرة (idea)، حيث يبدأ المُبرمج بفكرة عامة عن مُهِمّة (task) معينة يُريد من الحاسوب تَّنْفيذها، وعادةً ما يكون المُبرمج على عِلم بكيفية تَّنْفيذ تلك المُهِمّة يدويًا، أو لديه تَصَوُّر عام على الأقل. تتمحور المشكلة حول طريقة تَحْوِيل هذا التَصَوُّر العام إلى إِجراء (procedure)، واضح، ومُكتمِل، ومُحدَّد الخُطوات لتََّنْفيذ تلك المُهِمّة. يُسمَى مثل هذا الإِجراء باسم "الخوارزمية (algorithm)"، والتي هي إِجراء واضح، ومُحدَّد الخُطوات، والذي لابُدّ أن ينتهي دائمًا بَعْد عَدَد مُتناهي (finite number) من الخُطوات؛ فنحن لا نُريد عدّ الإِجراءات التي قد تَستمِر للأبد. لاحِظ أن الخوارزمية (algorithm) تَختلف عن البرنامج (program)، فالبرنامج يُكتَب بلغة برمجية معينة، أما الخوارزمية، فتُكتَب بأيّ لغة بما فيها الإنجليزية، وهي أَشْبَه بالفكرة وراء البرنامج، ويُقصَد بالفكرة هنا مجموعة الخُطوات المطلوب القيام بها حتى يَتمّ تَّنْفيذ المُهِمّة، وليس مُجرَّد مُلخَّص لما ينبغي للمُهِمّة إِنجازه في النهاية. عند وصف الخوارزمية، ليس من الضروري أن تَكُون الخُطوات مُفَصَّلة، وذلك طالما كانت واضحة بما فيه الكفاية لبَيَان أن تَّنْفيذها سوف يُنجِز المُهِمّة المطلوبة، ولكن بالطبع لا يُمكِن التعبير عنها كبرنامج (program) فِعليّ بدون مَلْئ جميع تلك التفاصيل. من أين تأتي الخوارزميات؟ ينبغي عادةً تطويرها، وهو ما يَتطلَّب كثيرًا من التفكير والعمل الجاد. يُمكِن القول أن عملية تطوير الخوارزميات (algorithm development) هي مهارة تُكتسَب مع المُمارسة المستمرة، ولكن، مع ذلك، تَتَوفَّر بعض التقنيات (techniques) والقواعد الإرشادية (guidelines) التي يُمكِنها مُساعدتك. سنتناول في هذا القسم بَعضًا منها، وبالأخص ما يَتعلَّق بالبرمجة "في نطاق ضيق"، كما سنعود للحديث عن نفس هذا الموضوع أكثر من مرة بالفصول القادمة. الشيفرة الوهمية والتصميم المتدرج عند البرمجة "في نطاق ضيق"، فأنت مُقيَّد نوعًا ما باِستخدَام عَدَد قليل من الأوامر، وهي كالتالي: المُتَغيِّرات (variables)، وتَعْليمَات الإِسْناد (assignment statements)، والبرامج (routines) الخاصة بعَمليتي الإِدْخال (input) والإِخراج (output). قد يَتوفَّر لك أيضًا اِستخدَام بعض البرامج الفرعية (subroutines)، والكائنات (objects)، أو رُبما حتى بعض اللَبِنات الأساسية الآخرى، ولكن بشَّرْط أن تَكُون قد كُتِبَت مُسْبَّقًا إِمّا بواسطتك أو بواسطة شخص آخر (لاحِظ أن برامج الإِدْخال والإِخراج تَقع ضِمْن هذا التصنيف). والآن، تَستطِيع بناء مُتتالية مِنْ تلك التَعْليمَات البسيطة، أو ربما قد تَدمجهم دِاخل بُنَى تحكُّم (control structures) أكثر تعقيدًا، مثل تَعْليمَة حَلْقة التَكْرار (loops) while، وتَعْليمَة التَفْرِيع الشَّرْطيّة if. لنفْترِض أننا نريد برمجة الحاسوب ليُنفِّذ مُهِمّة (task) معينة، يُمكِننا البدء بكتابة تَوصِيف (description/specification) مَبدئي لتلك المُهِمّة، بحيث يُلخِّص وظيفة الخوارزمية (algorithm) المطلوب تَطْويرها، ثُمَّ نُضيف مزيدًا من الخطوات والتفاصيل إلى ذلك التَوصِيف تدريجيًا، وعبر سِلسِلة من التصميمات المُحسَّنة، إلى أن نَصِل إلى الخوارزمية الكاملة التي يُمكِن ترجمتها مباشرة إلى لغة برمجية. تُكتَب عادة تلك التَوصِيفات باِستخدَام ما يُعرَف باسم الشيفرة الوهمية (pseudocode)، وهي مجموعة من التَعْليمَات العَاميَّة، التي تُحاكِي بِنْية اللغات البرمجية، بصورة مُبسَّطة، وبدون قواعد الصيغة (syntax) الصارمة المُعتادة بالشيفرة الفعليّة. يُطلَق على هذا الأسلوب من كتابة البرامج اسم التصميم المُتدرج (stepwise refinement)، ويُصنَّف ضِمْن استراتيجيات التصميم من أعلى لأسفل (top-down design). لنفْحَص كيفية تَطبيق التصميم المتدرج لكتابة إحدى البرامج التي قد تَعرَّضنا لها خلال القسم السابق. يَحسِب هذا البرنامج قيمة الاستثمار خلال خمسة أعوام. يُمكِن تَوصِيف مُهِمّة البرنامج بالعبارة التالية: "اِحسب قيمة الاستثمار واطبعها لكل عام من الأعوام الخمسة التالية، بحيث تُحدَّد قيمة الاستثمار المبدئي وسعر الفائدة مِن قِبَل المُستخدِم". ربما نُفكر بكتابة الشيفرة الوهمية التالية: // اقرأ مدخل المستخدم Get the user's input // احسب قيمة الاستثمار بعد عام Compute the value of the investment after 1 year // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد عامين Compute the value after 2 years // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد ثلاث أعوام Compute the value after 3 years // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد أربع أعوام Compute the value after 4 years // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد خمس أعوام Compute the value after 5 years // اطبع القيمة Display the value على الرغم من أن الخوارزمية بالأعلى سليمة وستؤدي الغرض منها، لكنها مُكرَّرة، وهو ما يَعنِي ضرورة اِستخدَام حَلْقة تَكْرار (loop)؛ لأنها ستُمكِّننا من كتابة شيفرة مُعمَّمة أكثر وبعَدَد سطور أقل. يُقصَد بالتَعْميم (generalization) هنا أنه يُمكِن اِستخدَام نفس حَلْقة التَكْرار بغض النظر عن عدد الأعوام المطلوب مُعالجتها. والآن، سنُعيد كتابة مُتتالية الخطوات السابقة كالتالي: // اقرأ مدخل المستخدم Get the user's input // طالما ما يزال هناك عدد من الأعوام للمعالجة while there are more years to process: // احسب قيمة الاستثمار بعد العام التالي Compute the value after the next year // اطبع القيمة Display the value على الرغم من أن الخوارزمية (algorithm) بالأعلى سليمة، لكنها مُوجَزة، وربما مُبهَمة بشكل أكثر من اللازم. يَحتاج الحاسوب عمومًا إلى تَعْليمَات واضحة وصريحة، ولهذا سنحتاج، مثلًا، إلى شرح الخطوات: "اقرأ مُدْخَل المُستخدِم" و "احسب قيمة الاستثمار بَعْد العام التالي" و "ما يزال هناك عَدَد من الأعوام للمعالجة". فمثلًا، نستطيع إعادة تَوصِيف الخطوة "اِقْرأ مُدْخَل المُستخدِم" إلى التالي: // اسأل المستخدم عن قيمة الاستثمار المبدئي Ask the user for the initial investment // اقرأ مدخل المستخدم Read the user's response // اسأل المستخدم عن قيمة سعر الفائدة Ask the user for the interest rate // اقرأ مدخل المستخدم Read the user's response أمَا بخُصوص الخطوة "احسب قيمة الاستثمار بَعْد العام التالي"، فسنحتاج إلى معرفة طريقة حِسَابها (ينبغي أن تَطلُب في تلك الحالة مزيد من التوضيح من أستاذك أو مُديرك)، ولكن دَعْنَا الآن نفْترِض أن قيمة الاستثمار تُحسَب بإضافة قيمة فائدة معينة إلى قيمة الاستثمار السابقة، وبالتالي يُمكِننا إِعادة كتابة حَلْقة التَكْرار while كالتالي: // طالما ما يزال هناك عدد من الأعوام للمعالجة while there are more years to process: // احسب الفائدة Compute the interest // أضف الفائدة إلى قيمة الاستثمار Add the interest to the value // اطبع القيمة Display the value نحتاج الآن إلى توضيح الاختبار الموجود بالخطوة "ما يزال هناك عَدَد من الأعوام للمعالجة"، وهو ما يُمكِن القيام به عن طريق عدّ الأعوام بأنفسنا، سنَستخدِم عَدَّادًا قيمته تُساوِي الصفر، ثم نُزيِد قيمة هذا العَدَّاد بمقدار الواحد بَعْد كل مرة نُعالِج فيها عامًا جديدًا، ونَتوقَف عندما تُصبِح قيمة العَدَّاد مُساوِية للعَدَد المطلوب من الأعوام. يُطلَق على ذلك عادةً اسم "حَلْقة العدّ (counting loop)"، وهي أحد الأنماط الشائعة، ولذلك تَوقَّع أن تَستخدِم شيئًا مُشابهًا بكثير من البرامج. تُصبِح الآن حَلْقة التَكْرار while كالتالي: // ابدأ بعدد أعوام يساوي الصفر years = 0 // طالما عدد الأعوام أقل من الخمسة while years < 5: // أزد عدد الأعوام بمقدار الواحد years = years + 1 // احسب الفائدة Compute the interest // أضف الفائدة إلى تلك القيمة Add the interest to the value // اطبع القيمة Display the value نَحتاج إلى أن نكون أكثر توضيحًا بخُصوص طريقة حِسَاب الفائدة، وسنفْترِض أنها تُساوِي حاصل ضرب سعر الفائدة بقيمة الاستثمار الحالية. نُضيِف هذا الإيضاح إلى ذلك الجزء من الخوارزمية المسئول عن قراءة مُدْخَلات المُستخدِم، وبهذا، نَحصُل على الخوارزمية الكاملة: // اسأل المستخدم عن قيمة الاستثمار المبدئي Ask the user for the initial investment // اقرأ مدخل المستخدم Read the user's response // اسأل المستخدم عن قيمة سعر الفائدة Ask the user for the interest rate // اقرأ مدخل المستخدم Read the user's response // ابدأ بعدد أعوام يساوي الصفر years = 0 // طالما عدد الأعوام أقل من الخمسة while years < 5: // أزد عدد الأعوام بمقدار الواحد years = years + 1 // احسب الفائدة بحيث تساوي حاصل ضرب القيمة مع سعر الفائدة Compute interest = value * interest rate // أضف الفائدة إلى تلك القيمة Add the interest to the value // اطبع القيمة Display the value وَصلنا إلى النقطة التي يُمكِن معها الترجمة المُباشرة إلى لغة برمجة مناسبة، فقط نحتاج إلى اِختيار أسماء المُتَغيِّرات (variables)، وتَقْرير نص العبارات التي سنَطبَعها للمُستخدِم، وهكذا. نستطيع الآن كتابة الخوارزمية (algorithm) بلغة الجافا كالتالي: double principal, rate, interest; // التصريح عن المتغيرات int years; System.out.print("Type initial investment: "); principal = TextIO.getlnDouble(); System.out.print("Type interest rate: "); rate = TextIO.getlnDouble(); years = 0; while (years < 5) { years = years + 1; interest = principal * rate; principal = principal + interest; System.out.println(principal); } ما زال أمامنا بعض التَحسِّينات الإضافية، مِن بينها تَضْمِين هذه الشيفرة داخل برنامج كامل، وإضافة التعليقات (comments)، وطِباعة المَزيد من المعلومات للمُستخدِم، ولكنه يظِلّ نفس البرنامج بالقسم السابق. في حين تَستخدِم خوارزمية الشيفرة الوهمية (pseudocode algorithm) المسافات البادئة (indentation) لتوضيح التَعْليمَات الواقعة ضِمْن حَلْقة التَكْرار (loop)، تُهمِل لغة الجافا هذه المسافات البادئة تمامًا، ولهذا أَضفنا قوسين معقوصين (curly brackets/braces) {} لتوضيح أيّ مجموعة تَعْليمَات (statements) تقع ضِمْن حَلْقة التَكْرار. إذا لم تَستخدِم هذه الأقواس بشيفرة الجافا، فإن الحاسوب سيفْترِض أن التَعْليمَة الوحيدة الواقعة ضِمْن حَلْقة التَكْرار هي years = years + 1;، أمَا بقية التَعْليمَات فسيُنفِّذها مرة واحدة فقط بَعْد انتهاء حَلْقة التَكْرار. للأسف، لا يُبلِّغ الحاسوب عن هذه النوعية من الأخطاء، بنفس الطريقة التي يُبلِّغ بها عن حُدوث خطأ في حالة عدم اِستخدَام القوسين الهلاليين (rounded brackets/parentheses) () حول (years < 5)؛ وذلك لأن تلك الأقواس مطلوبة وِفقًا لصيغة (syntax) تَعْليمَة while، أمَا قوسيّ المعقوصين {}، فإنها مطلوبة فقط لأغراض دَلاليّة (semantics)، أيّ أغراض مُتعلِّقة بالمعنى. يَستطيع الحاسوب عمومًا تَمييز أخطاء بناء الجملة (syntax errors) فقط، لا الأخطاء الدَلاليّة (semantic errors). لاحِظ أن التَوصِيف الأصلي للمسألة التالي لم يَكُن مُكتملًا: ينبغي لك عمومًا، قَبْل بدء كتابة أيّ برنامج، أن تتأكد من أن لديك التَوصِيف الكامل لوظيفة البرنامج المطلوب كتابته، فلابُدّ أن تَعرِف المعلومات التي سيَقرأها البرنامج (input) وأيّ خَرْج (output) ينبغي أن يَطبعُه، وكذلك الحِسَابات التي ينبغي له القيام بها. ربما يُمكِننا إعادة تَوصِيف نفس البرنامج السابق بصورة أكثر معقولية كالتالي: مسألة متتالية "3N+1" لنفْحَص مثالًا آخرًا لم نتَعرَّض له من قَبْل، السؤال هنا عبارة عن مَسألة رياضية مُجرَّدة، والتي يَعُدّها الكاتب تَحْدِيدًا واحدة مِن تمارينه البرمجية المُفضلة. سنبدأ هذه المرة، بعكس المثال السابق، بتَوصِيف كامل (specification) لمُهِمّة (task) البرنامج: اُكْتُب برنامجًا يَقرأ عَدَدًا صحيحًا موجبًا من المُستخدِم، ثُمَّ يَطبَع مُتتالية الأعداد "3N+1"، بحيث تبدأ من العَدَد المُدْخَل، كما يَنبغي للبرنامج أن يعدّ عَدَد عناصر المُتتالية ويَطبعها." اُنظر الخوارزمية المبدئية التالية، والتي تُوضِح فقط التَصَوُّر العام لمثل هذا البرنامج: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user. // احسب قيمة كل عنصر بالمتتالية، واطبعه وعدّه Compute, print, and count each number in the sequence. // اطبع عدد عناصر المتتالية Output the number of terms. يَتضح لنا أن الخطوة الثانية تَحتوِي على المَضمون الفِعليّ للبرنامج، وبالطبع تحتاج إلى مزيد من الإيضاح. لمّا كنا نُريد الاستمرار بحِسَاب قيم عناصر المُتتالية حتى تُصبِح قيمة N الحالية مُساوِية للعَدَد ١، فإننا سنحتاج ببساطة إلى اِستخدَام حَلْقة تَكْرار (loop)، ولذلك دَعْنَا نُعيد صياغة نفس الجملة السابقة بحيث تَتوافق مع حَلْقة التَكْرار while. إننا في حاجة إلى مَعرِفة متى نَستمِر بتَّنْفيذ حَلْقة التَكْرار ومتى نُوقِّفها، في الواقع، سنستمر طالما كانت قيمة N الحالية لا تُساوِي ١، ولهذا يُمكِننا إعادة كتابة خوارزمية الشيفرة الوهمية (pseudocode algorithm) كالتالي: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user; // طالما كانت قيمة `N` الحالية لا تساوي 1 while N is not 1: // احسب قيمة عنصر المتتالية التالي واسنده إلى N Compute N = next term; // اطبع قيمة N Output N; // عدّ عنصر المتتالية Count this term; // اطبع عدد عناصر المتتالية Output the number of terms; لمّا كان حِسَاب قيمة عنصر المُتتالية التالي يَعتمِد على ما إذا كانت قيمة N الحالية هي عَدَد زوجي (even) أم فردي (odd)، يَعنِي ذلك أن الحاسوب بحاجة إلى تَّنْفيذ حَدَثين مُختلفين لكل حالة، وهو ما يَعنِي أن اِستخدَام تَعْليمَة التَفْرِيع الشَّرْطيّة if بات ضروريًا؛ وذلك للاختيار ما بين تلك الحالتين، اُنظر الخوارزمية بَعْد التعديل: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user; // طالما كانت قيمة `N` الحالية لا تساوي 1 while N is not 1: // إذا كان N عددًا زوجيًا if N is even: // احسب قيمة العنصر التالي وأسنده إلى N Compute N = N/2; // إذا كان N عددًا فرديًا else // احسب قيمة العنصر التالي وأسنده إلى N Compute N = 3 * N + 1; // اطبع قيمة N Output N; // عدّ عنصر المتتالية Count this term; // اطبع عدد عناصر المتتالية Output the number of terms; انتهينا تقريبًا، يَتبقَّى فقط العدّ (counting)؛ وذلك لطباعة عَدَد عناصر المُتتالية. يَعنِي العدّ ببساطة أن تبدأ بالقيمة صفر، ثم تُضيف المقدار واحد في كل مرة يَكُون لديك فيها ما تعدّه، ولهذا نحتاج إلى مُتَغيِّر (variable) للقيام بالعدّ، يُعرَف باسم العَدَّاد (counter). يَنبغي ضَبْط قيمة ذلك المُتَغيِّر إلى القيمة صفر قَبْل بداية الحَلْقة (loop)، بحيث تَزداد (increment) تلك القيمة أثناء تَّنْفيذ الحَلْقة. (يُعدّ ذلك أحد الأنماط الشائعة [common pattern]، ولذلك تَوقَّع أن تراه بكثير من البرامج). تُصبِح الخوارزمية، بَعْد إضافة العَدَّاد (counter)، كالتالي: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user; // اضبط قيمة العداد إلى القيمة صفر Let counter = 0; // طالما كانت قيمة N الحالية لا تساوي 1 while N is not 1: // إذا كان N عددًا زوجيًا if N is even: // احسب قيمة العنصر التالي وأسنده إلى N Compute N = N/2; // إذا كان N عددًا فرديًا else // احسب قيمة العنصر التالي وأسنده إلى N Compute N = 3 * N + 1; // اطبع قيمة N Output N; // أزد قيمة العداد بمقدار 1 Add 1 to counter; // اطبع عدد عناصر المتتالية Output the counter; ما يزال أمامنا مشكلة أخيرة بخُصوص الخطوة الأولى، وهي كيف نتأكد من أن المُستخدِم قد أَدْخَل عَدَدًا صحيحًا موجبًا؟ ففي الواقع، قد يُدْخِل المُستخدِم عَدَدًا سالبًا أو صفرًا، وعندها سيستمر تَّنْفيذ البرنامج للأبد؛ لأن القيمة المُدْخَلة N، في تلك الحالة، لن تُصبِح أبدًا مُساوِية للواحد. ربما قد لا يُعدّ ذلك مشكلة ضخمة في تلك الحالة تَحْدِيدًا، ولكن، مع ذلك، ينبغي عمومًا محاولة كتابة برامج غَيْر قابلة للخطأ. نستطيع حَل تلك المُشكلة عن طريق الاستمرار بقراءة الأعداد إلى أن يُدْخِل المُستخدِم عددًا صحيحًا موجبًا. // اطلب من المستخدم إدخال عدد صحيح موجب Ask user to input a positive number; // أسند القيمة المدخلة إلى N Let N be the user's response; // طالما N ليست موجبة while N is not positive: // اطبع رسالة خطأ Print an error message; // اقرأ قيمة أخرى واسندها إلى N Read another value for N; // اضبط قيمة العداد إلى القيمة صفر Let counter = 0; // طالما كانت قيمة `N` الحالية لا تساوي 1 while N is not 1: // إذا كان N عددًا زوجيًا if N is even: // احسب قيمة العنصر التالي وأسنده إلى N Compute N = N/2; // إذا كان N عددًا فرديًا else // احسب قيمة العنصر التالي وأسنده إلى N Compute N = 3 * N + 1; // اطبع قيمة N Output N; // أزد قيمة العداد بمقدار 1 Add 1 to counter; // اطبع عدد عناصر المتتالية Output the counter; لاحِظ أن حَلْقة while الأولى ستنتهي فقط عندما تُصبِح قيمة N زوجية. عند محاولة كتابة شيفرة التَوصِيف التالي: "إذا كانت قيمة N غَيْر زوجية، اُطلب من المُستخدِم إِدْخَال عدد آخر"، يقع الكثير من المبرمجين، وبالأخص المبتدئين، في خطأ اِستخدَام تَعْليمَة التَفْرِيع if بدلًا من تَعْليمَة حَلْقة التَكْرار while. تَظهر المشكلة تَحْدِيدًا عندما يُدْخِل المُستخدِم عددًا غَيْر زوجي مرة آخرى. لمّا كانت تَعْليمَة التَفْرِيع if تُنفَّذ مرة واحدة فقط، فإنه لا يَتمّ فَحْص مُدْخَل المُستخدِم إلا مرة واحدة فقط، مما يعني أن البرنامج سينتقل إلى تَّنْفيذ التَعْليمَة التالية بغض النظر عما إذا كانت قيمة المُدْخَل الثاني للمُستخدِم زوجية أم لا، وهو ما يَتسبَّب بحُدوث حَلْقة لا نهائية (infinite loop) كما ذَكرنا آنفًا. أمَا في حالة اِستخدَام حَلْقة التَكْرار while، فإن الحاسوب سيَقفِز (أو سيَنقِل التَحكُّم بتعبير أدق) إلى بداية الحَلْقة بَعْد كل عملية إِدْخَال؛ لاختبار ما إذا كانت القيمة المُدْخَلة زوجية أم لا، مما يَعنِي أنه سيستمر في طلب إِدْخَال عَدَد جديد إلى أن يُدْخِل المُستخدِم قيمة مقبولة، أيّ عدد زوجي. وبالتالي، في حالة انتقال البرنامج إلى تَّنْفيذ ما بَعْد حَلْقة while، فإن قيمة N هي زوجية حتمًا. ها هو نفس البرنامج بشيفرة الجافا. لاحِظ اِستخدَام العَامِلين (operators) <= بمعنى "أقل من أو يُساوِي" و != بمعنى "لا يُساوِي"، بالإضافة إلى اِستخدَام التعبير N % 2 == 0؛ لاختبار ما إذا كانت قيمة N زوجية. نُوقِشَت كل هذه العَوامِل في القسم ٢.٥. import textio.TextIO; public class ThreeN1 { public static void main(String[] args) { int N; // لحساب العناصر بالمتتالية int counter; // لعد عدد عناصر المتتالية System.out.print("Starting point for sequence: "); N = TextIO.getlnInt(); while (N <= 0) { System.out.print( "The starting point must be positive. Please try again: " ); N = TextIO.getlnInt(); } // نعلم أن N هي عدد صحيح موجب عند هذه النقطة counter = 0; while (N != 1) { if (N % 2 == 0) N = N / 2; else N = 3 * N + 1; System.out.println(N); counter = counter + 1; } System.out.println(); System.out.print("There were "); System.out.print(counter); System.out.println(" terms in the sequence."); } // نهاية main } // نهاية الصنف ThreeN1 مُلاحظتان أخيرتان على هذا البرنامج: أولًا، ربما لاحَظت أن البرنامج لم يَطبَع قيمة أول عنصر بالمُتتالية -أيّ قيمة N المُدْخَلة من قِبَل المُستخدِم-، وكذلك لم يعدّها. هل هذا خطأ؟ يَصعُب القول. ربما ينبغي أن نَطرح سؤالًا آخر: هل كان تَوصِيف البرنامج (specification) صريحًا بما يَكفي بخُصوص تلك النقطة؟ في الواقع، للإجابة على مثل هذا السؤال، ستَحتاج إلى طَلَب مزيد من الإيضاح من أستاذك/مديرك. يُمكِن عمومًا حل هذه المشكلة -في حال كانت- بسهولة، فقط اِستبدل السَطْرين التاليين بتَعْليمَة counter = 0 قَبْل حَلْقة التَكْرار while : System.out.println(N); // print out initial term counter = 1; // and count it ثانيًا، لماذا تُعدّ هذه المسألة تَحْدِيدًا مثيرة؟ في الواقع، يَجِدْ كثير من علماء الرياضيات والحاسوب هذه المسألة مُشوقة؛ بسبب سؤال بسيط، يَخُص تلك المسألة، والذي لم يَتوصَّلوا للإجابة عليه بَعْد. السؤال هو "هل عملية حِسَاب قيم مُتتالية '3N+1' دومًا ما ستنتهي بَعْد عَدَد مُتناهي (finite) من الخطوات لجميع قيم N المبدئية؟" على الرغم من سهولة حِسَاب قيم المُتتاليات بشكل مُفرد، لم يُجِبْ أحد على السؤال الأعم حتى الآن، أيّ بصياغة آخرى، لا أحد يَعلم ما إذا كان من الصحيح تسمية عملية حِسَاب قيم مُتتالية "3N+1" بـ"الخوارزمية (algorithm)"؛ فبالنهاية، لابُدّ لأيّ خوارزمية أن تَنتهي بَعْد عَدَد مُتناهي من الخطوات. لاحظ: يَنطبق ذلك على الأعداد الصحيحة (integers) بمفهومها الرياضي، وليس القيم من النوع العددي الصحيح int! بمعنى أننا نفْترِض هنا أن قيمة N قد تَكُون أيّ عدد صحيح مُمكن مهما كَبُر، وهو ما لا يَنطبق على مُتَغيِّر من النوع int داخل برنامج بلغة الجافا. إذا أَصبحت قيمة N كبيرة جدًا ليَتمّ تَمثيلها بمُتَغيِّر من النوع int (32 بت)، فلا يُمكِن عدّ قيم خَرْج البرنامج صحيحة رياضيًا، أيّ أن البرنامج لا يَحسِب قيم متتالية "3N+1" بشكل صحيح عندما تَكُون قيمة N كبيرة. اُنظر تمرين ٨.٢. كتابة الشيفرة (coding) والاختبار (testing) وتنقيح الأخطاء (debugging) بَعْد انتهائك من تَطوير خوارزمية البرنامج (algorithm)، سيَكُون من اللطيف لو كان بإمكانك الضغط فقط على زِر معين؛ لتَحصُل بَعْدها على برنامج قابل للتَّنْفيذ (working program) بصورة ممتازة. في الواقع، عملية تَحْوِيل الخوارزمية إلى شيفرة بلغة الجافا لا تَتمّ دومًا بمثل هذه السَلاسَة لسوء الحظ، وحتى عندما تَصِل إلى تلك المرحلة من الحُصول على برنامج قابل للتَّنْفيذ (working program)، فإنه غالبًا ما يَكُون قابلًا للتَّنْفيذ بمعنى أنه يُنفِّذ "شيء ما"، لا بالضرورة الشيء الذي تريده أن يَفعَله. بَعْد الانتهاء من تصميم البرنامج (program design)، يَحيِن موعد كتابة الشيفرة (coding): أيّ ترجمة التصميم إلى برنامج مكتوب بلغة الجافا أو بأيّ لغة برمجية اخرى. مَهمَا كنت حَريصًا أثناء كتابة الشيفرة، فعادةً ما ستَجِدْ بعض أخطاء بناء الجملة (syntax errors) طريقها إلى الشيفرة، ولذلك سيَرفُض مُصرِّف (compiler) الجافا البرنامج، وسيُبلِّغك عن نوع معين من رسائل الخطأ (error message). لاحِظ أنه في حين يستطيع المُصرِّف اكتشاف أخطاء بناء الجملة (syntax errors) دائمًا، فإنه لسوء الحظ ليس بنفس الكفاءة في اكتشاف مَاهية الخطأ، بل أنه قد لا يَتَمكَّن، في بعض الأحيان، من معرفة مكان حُدوث الخطأ الفِعليّ، فمثلًا، قد يَتسبَّب وجود خطأ إملائي أو نَسْيَان قوس "{" بالسطر رقم ٤٥ بتَوَقُّف المُصرِّف بالسطر ١٠٥. يَظِلّ، مع ذلك، الفهم الجيد لقواعد صياغة (syntax rules) اللغة البرمجية مع اِتباع بعض القواعد الإرشادية البرمجية البسيطة الطريقة الأفضل لتَلافِي كثير من تلك الأخطاء. لنسْتَعْرِض بعضًا من تلك القواعد، أولًا، لا تَكتُب أبدًا قوس حَاصِرة "{" بدون كتابة زوجه الآخر "}"، ثُمَّ عُد بَعْدها لكتابة التَعْليمَات بينهما؛ وذلك لأن نَسْيَان قوس أو إضافة قوس في غَيْر مَحَلّه يُعدّ من أكثر الأخطاء التي يَصعُب اكتشافها خاصة بالبرامج الضخمة. ثانيًا، اِستخدِم دائما المسافات البادئة (indentation) لتَنسيق الشيفرة، وإن عَدَّلت البرنامج، عَدِّل أيضًا المسافات البادئة بحيث تُصبِح مُتوافقة مع التَعْدِيل الجديد. ثالثًا، اِستخدِم نَمط تَسمية (naming scheme) ثابت؛ حتى لا تُعانِي بَعْد ذلك بينما تَتَذكَّر ما إذا كان اسم مُتَغيِّر ما (variable) هو "interestrate" أم "interestRate". رابعًا، عندما يُبلِّغك المُصرِّف بأكثر من رسالة خطأ (error message) واحدة، لا تُحاوِل إصلاح رسالة الخطأ الثانية حتى تَنتهي من إِصلاح الأولى؛ لأن المُصرِّف عادةً ما يَرتبك بعد إِيجاده لأول خطأ، ولذلك قد تَكُون رسائل الخطأ التالية هي مُجرَّد تَخمينات. وأخيرًا، وربما هي النصيحة الأفضل: خُذ الوقت الكافي لفِهم الخطأ قبل مُحاولة إِصلاحه؛ فالبرمجة، بالنهاية، ليست علمًا تجريبيًا (experimental science). إذا تمّ تَصرِيف برنامجك بنجاح، لا يَعنِي ذلك أنك قد انتهيت؛ فمن الضروري أن تَختبر (test) البرنامج لتتأكد مما إذا كان يَعمَل بشكل صحيح، وهو ما لا يَقْتصِر على مُجرَّد الحُصول على الخَرْج الصحيح (output) لعينة المُدْخَلات (inputs) التي أَعطاك إِياها الأستاذ، بل ينبغي للبرنامج أن يَعمَل بشكل سليم لجميع المُدْخَلات المقبولة، وفي حالة اِستقباله لمُدَْخَل غَيْر صالح، فينبغي أن يُوبِخ البرنامج المُستخدِم بلطف، لا أن يَنَهار (crashing) تمامًا. عمومًا، ينبغي أن تَختبِر البرنامج على نطاق واسع من المُدْخَلات. قد تُحاوِل أيضًا إِيجاد مجموعة المُدْخَلات التي بإِمكانها اِختبار جميع الوظائف التي أَدْرَجتها بالشيفرة. في حالة كتابة برامج كبيرة، حَاوِل تقسيمها إلى عدة مراحل، بحيث تَختبِر كل مرحلة قَبْل البدء بالمرحلة التالية، حتى لو اضطررت لكتابة شيفرة إِضافية تقوم بالاختبار مثل أن تَستدعِي أحد البرامج الفرعية التي قُمت بكتابتها للتو؛ فأنت حتمًا لا تُريد أن يَنتهي بك الحال بخُمسمائة سَطْر جديد من الشيفرة مَصحوبة بخطأ ما في مكان ما. الغرض من الاختبار (testing) هو مُحاولة العُثور على الأخطاء البرمجية (bugs)، وهي -بعكس أخطاء التَصرِيف (compilation errors)- أخطاء دَلاليّة (semantic errors)، أيّ تَكُون في صورة سُلوك غَيْر سليم. المُحزن في الأمر هو أنك غالبًا ما ستَجِدهم. تستطيع، مع ذلك، تَقْليل -لا التَفادِي التام- هذه النوعية من الأخطاء البرمجية (bugs)، من خلال الانتباه أثناء قيامك بكُلًا من التصميم (design)، وكتابة الشيفرة (coding). عندما تَكتشف خطأً برمجيًا (bug)، يَحيِن موعد تَنْقِيح الأخطاء (debugging)، بمعنى تَعَقُّب سبب الخطأ بالشيفرة بهدف التخلص منه. لاحظ أنك لن تُصبِح خبيرًا بتَنْقِيح الأخطاء إلا من خلال المُمارسة المستمرة؛ فهي مَهارة تكتسبها، مثلها مثل جميع نواحي البرمجة الآخرى، ولذلك لا تَكُن خائفا منها، وإنما تَعلَّم منها. تُعدّ القدرة على قراءة الشيفرة واحدة من أهم مهارات تَنْقِيح الأخطاء الاساسية، والمقصود بقراءة الشيفرة هنا: القدرة على تَنْحية تَصوراتك المُسْبَّقة عما ينبغي للشيفرة أن تقوم به، وبدلًا من ذلك، تَعقُّب الطريقة التي يُنفِّذها بها الحاسوب خَطوة بخَطوة؛ لتُدرِك ما تَقوم به فِعليًا. في الواقع، هذا ليس بالأمر السهل. ما يزال الكاتب يَذكُر تلك المرة التي قضى فيها عدة ساعات يَبحث عن خطأ برمجي ليَكتشِف بالنهاية أن سَطْرًا ما بالشيفرة، كان قد نَظَر إليه عشرات المرات، يَحتوِي على القيمة ١ بدلًا من الحرف i، أو تلك المرة التي كَتَب فيها برنامجًا فرعيًا (subroutine) اسمه هو WindowClosing، والذي كان سيُؤدي غرضه تمامًا لولا أن الحاسوب كان يَبحث عن البرنامج الفرعي windowClosing (بحرف w صغير). قد يُساعِدك أحيانًا الاستعانة بشخص آخر لفَحْص الشيفرة، خاصة وأنه لن يَملك نفس تَصوراتك المُسْبَّقة عنها. أحيانًا ما يَكُون مُجرَّد العثور على ذلك الجزء من البرنامج الذي يَكمُن فيه الخطأ مُشكلة بحد ذاته، ولهذا تُوفِّر مُعظم بيئات التطوير البرمجية (programming environments) برنامجًا يُسمَى مُنقِّح الأخطاء (debugger)؛ لمساعدتك بالعثور على الأخطاء البرمجية (bugs)، بحيث يَتمّ تَشْغِيل البرنامج (program) تحت تَحكُّم ذلك المُنقِّح، والذي يَسمَح لك بضَبْط ما يُعرَف باسم نقاط المُقاطعة (breakpoints)، وهي نقاط بالبرنامج سيَتوقَّف عندها المُنقِّح مؤقتًا (pause)؛ حتى تَتَمكَّن من فَحْص قيم مُتَغيِّرات البرنامج عند تلك النقطة، مما سيُساعدك في العُثور على المكان الذي بدأت فيه الأخطاء البرمجية بالظهور أثناء تَّنْفيذ البرنامج. بمُجرَّد تَحْدِيد ذلك الجزء من البرنامج الذي يَكمُن فيه الخطأ البرمجي (bug)، يَسمَح لك المُنقِّح أيضًا بتَّنْفيذ البرنامج سَطْرًا سَطْرًا، ومِنْ ثَمَّ، تستطيع مشاهدة ما يَحدُث تفصيليًا. يَعترِف الكاتب أنه لا يَستخدِم مُنقِّح الأخطاء دائمًا، وإنما يَتبِع المنهج التقليدي لتَنْقِيح الأخطاء، وذلك بإضافة تَعْليمَات تَنْقِيحيّة (debugging statements) داخل البرنامج، والتي هي مُجرَّد تَعْليمَات خَرْج تَطبَع معلومات عن حالة (state) البرنامج. عادةً ما تُكتَب أيّ تَعْليمَة تَنْقِيحيّة على الصورة التالية: System.out.println("At start of while loop, N = " + N); ينبغي لنص الخَرْج أن يُساعِدك على تَحْدِيد مكان تَعْليمَة الطباعة المسئولة عن ذلك الخَرْج، وكذلك معرفة قيم المُتَغيِّرات (variables) المُهمة. ستكتشف، في بعض الأحيان، أن الحاسوب لم يَمر حتى على ذلك الجزء من البرنامج الذي كنت تَظن أنه يُنفِّذه. تَذكَّر أن الهدف هو اكتشاف أول نُقطة بالبرنامج أَصبَحت فيها حالة البرنامج (state) مُخالِفة للحالة المُتوقَّعة، فهذا هو مكان الخطأ البرمجي (bug). وأخيرًا، تَذكَّر القاعدة الذهبية لتَنْقِيح الأخطاء: إذا كنت مُتأكدًا تمامًا أن كُل شئ بالبرنامج سليم، ومع ذلك ما يزال لا يَعمَل بالصورة المطلوبة، فلابُدّ أن واحدًا من تلك الأشياء هو ببساطة خطأ. ترجمة -بتصرّف- للقسم Section 3.1 Blocks, Loops, and Branches من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.1 نقطة
-
تعتمد قدرة الحاسب على أداء مهامٍ معقدة على دمج التعليمات البسيطة ضمن بنى تحكمٍ. هناك 6 بنى تحكم كهذه في جافا، تستخدم لتحديد تدفق التحكم الطبيعي في البرنامج وتكفي ثلاث منها لكتابة برامج قادرة على أداء أيّة مهمة.بنى التحكم الستّ هي: الكتلة (block) وحلقة while وحلقة do..while وحلقة for وتعليمة if وتعليمة switch. تُعدّ كلّ واحدة من البنى السابقة "تعليمةً" مفردة، لكنها في الواقع تعليمة بنيوية قد تحتوي بداخلها تعليمة أو أكثر. الكتل الكتلة (Blocks) هي الشكل الأبسط للتعليمات البنيوية، وتهدف إلى تجميع سلسلة من التعليمات في تعليمة واحدة. تأخذ الكتلة الشكل التالي: { // ضع التعليمات هنا } بكلمات أخرى، تتألف الكتلة من سلسلة من التعليمات المُغلّفة بين قوسين من الشكل "{ }". يمكن ألّا تحتوي الكتلة أيّة تعليمة، وتدعى عندئذٍ بالكتلة الفارغة (empty block) ومن الوارد أن تلزمك أحيانًا. تتألف الكتلة الفارغة من زوج فارغ من الأقواس فقط. عادةً ما ترد كتل التعليمات ضمن تعليمات أخرى وتهدف إلى تجميع عدة تعليمات معًا في وحدةٍ واحدة. يمكنك في الحقيقة استخدام كتل التعليمات أينما وردت تعليمةٌ. بيد أنّ من الواجب استخدامها في حالة البرامج الفرعية -لاحظ استخدامنا لها سابقًا في حالة البرنامج الفرعي main . البرنامج الفرعيّ هو كتلةٌ بالتعريف، نظرًا لكونه سلسلةً من التعليمات المغلّفة ضمن زوج من الأقواس. من الجدير بالذكر هنا أنّ لغة البرمجة جافا هي لغةٌ حرة التنسيق وهذا يعني عدم وجود قواعد صياغة تحدد كيفيّة ترتيب اللغة على الصفحة. يمكنك إن أردت، على سبيل المثال، كتابة كتلةٍ كاملة على سطر واحد. مع ذلك، ينبغي عليك، اتباعًا لممارسات البرمجة الفضلى، تنظيم برنامجك على نحوٍ يجعل قراءته وفهمه أسهل ما يمكن. يقتضي هذا في الحالة العامة كتابةً تعليمةٍ واحدة فقط في السطر واستخدام المسافات البادئة للإشارة إلى التعليمات المُحتواة ضمن بنى التحكم. سيُستخدم التنسيق هذا في جميع أمثلة الكتاب.إليك مثالين عن الكتل: { System.out.print("الجواب هو: "); System.out.println(ans); } { // تستبدل هذه الكتلة بين قيمتي المتغيرين x و y int temp; // مُتغير مؤقت لاستخدامه ضمن هذه الكتلة temp = x; // احفظ نسخةً عن x صمن temp. x = y; // انسخ قيمة y إلى x. y = temp; // انسخ قيمة temp إلى y. } قمنا في المثال الثاني بالتصريح عن المتغيّر temp ضمن الكتلة. التصريح عن متغيّرٍ ضمن كتلةٍ أمرٌ جائزٌ لا بل ومستحسنٌ أيضًا لا سيما إن كان استخدام المتغير مقتصرًا على تلك الكتلة. لا يمكن الوصول للمتغير المُصرّح عنه ضمن كتلة خارجها ولا يكون مرئيًّا إلا فيها. عندما ينفّذ الحاسوب تعليمة التصريح عن متغيّر، يخصص موضعًا في الذاكرة لتخزين قيمة هذا المتغيّر (نظريًّا على الأقل). عندما تنتهي الكتلة، يتم التخلّص من موضع الذاكرة ذاك ليصبح مُتاحًا للاستخدام مجددًا. يقال أنّ هذا المتغير "محلّي" ضمن الكتلة. هناك مفهوم أكثر شمولًا يُصطلح على تسميته مجال المُعرّف (scope of an identifier). مجال المُعرّف هو جزء من البرنامج يكون فيه المعرّف صالحًا. يقتصر مجال المتغيّر المُعرّف ضمن كتلة على الكتلة نفسها، وعلى نحوٍ أدق، يقتصر مجال المتغيّر المعرّف ضمن كتلة على الجزء من الكتلة الذي يلي التصريح عنه. حلقة while الأساسيّة لا تؤثر تعليمة الكتلة بحدّ ذاتها على تدفق التحكم في البرنامج على عكس بنى التحكم الخمس المتبقية. تُصنّف هذه البنى إلى نوعين: تعليمات الحلقات وتعليمات التفريع. في الواقع، لا تحتاج لغة البرمجة عامّة الغرض سوى إلى بنية تحكم واحدة من كل نوعٍ وكل ما سواهما هو بغرض التسهيل والتبسيط ليس إلّا. سنتطرق في هذا القسم إلى حلقة while وتعليمة if ونؤجل الحديث عن بنى التحكم الثلاث المتبقية إلى أقسام لاحقة. تُستخدم حلقة while لتكرار تعليمة معيّنة مرة بعد مرة. من غير المرجّح أنّك ستحتاج تكرار العملية إلى الأبد، لكنّه أمرٌ ممكن ويدعى الحلقة اللانهائية وهو غير محبّذ في الحالة العامة. هناك قصة قديمة عن رائدة علوم الحاسوب غريس موراي هوبر التي قرأت التعليمات على علبة الشامبو وكانت تنصّ على: " ارغي، اشطفي، كرري العمليّة." اتبعت غريس التعليمات حسب قولها وانتهى بها الأمر بعلبة شامبو فارغة. روت غريس القصة كمزحة حول اتباع الحواسيب للتعليمات بدون تفكير. توخيًّا للدقة، نقول أنّ حلقة while تُكرر التعليمة مرةً بعد مرة طالما أنّ قيمة شرطٍ محدّدٍ هي true. تأخذ حلقة while الشكل الآتي: while (تعبير منطقي) //ضع تعليماتك هنا نظرًا لأنّه من الممكن والمرّجح أن التعليمة هي كتلة تعليمات، تأخذ معظم حلقات while الشكل الآتي: while (تعبير منطقي) { //ضع تعليماتك هنا } يظّن بعض المبرمجين أنّ من الواجب تضمين الأقواس دائمًا لضرورات تنسيقية حتى لو كانت ستُغلّف تعليمةً واحدة فحسب، لكننا لن نتبع هذه النصيحة في الكتاب. أمّا دلالة تعليمة while فهي كما يلي: عندما يصادف الحاسوب تعليمة while، يقوم بتقييم التعبير المنطقي لينتج عن ذلك قيمة إما ture أو false. إذا كانت القيمة false، يتجاوز الحاسوب بقيّة حلقة while ويتابع تنفيذ الأمر الذي يليها في البرنامج. أمّا إذا كانت قيمة التعبير ture، ينفّذ الحاسوب التعليمة أو كتلة التعليمات الواردة ضمن الحلقة. بعدئذٍ، يعود الحاسوب إلى بداية حلقة while ويكرر العملية (أي يعاود تقييم التعبير المنطقي، يُنهي التنفيذ إذا كانت قيمته false، ويستمر إذا ما كانت ture). تتكرر العملية مرةً بعد مرةٍ بعد مرة حتى تصبح قيمة التعبير false عند قيام الحاسوب بتقييمه. إن لم نصل لهذه النتيجة، فستستمر الحلقة إلى الأبد.إليك مثالًا عن حلقة while بسيطة تطبع الأرقام 1، 2، 3، 4، 5: int number; // العدد المراد طباعته. number = 1; // ابدأ من الرقم 1. while ( number < 6 ) { // تابع طالما أن العدد أصغر من 6. System.out.println(number); number = number + 1; // انتقل للعدد التالي. } System.out.println("انتهى التنفيذ"); ترجمة -بتصرّف- للقسم Section 2: Algorithm Development من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.1 نقطة
-
هل أنت مهتّمٌ بإنشاء إضافة ووردبريس خاصّة بك من الصّفر؟ إن لم تكُن قد قمت ببرمجة إضافة خاصّة بك من قبل فقد يبدو لك ذلك كالتوغل في عالم مجهولٍ، خاصّة إن لم تكُن تثق بمهاراتك في لغة PHP. التحدّي الذي ستواجهه سيكون معرفتك من أينَ ستبدأ، بالإضافة إلى قدرتك على تقبّل إمكانيّة ارتكابك لعدّة أخطاءٍ طيلة مسار التعلّم. الإضافات تسمح لك بإضافة جميع أنواع الوظائف الممكنة للووردبريس، انطلاقًا من إضافة استمارة تواصلٍ بسيطةٍ إلى صفحة ما لتعزيز حماية موقعك وُصولًا إلى إضافة قدرات تجارةٍ إلكترونية. تقريبًا يوجد إضافة لأيِّ شيءٍ يخطُر على بالك، إن لم يكن كذلك فيمكنك دائمًا إنشاء واحدة وفق تصوّراتك وحاجاتك. في هذا الدّرس ستتعلّمٌ طريقة برمجة إضافتك الخاصّة لكي تتمكّنً من تعديل وظائف: موقعك الووردبريس أو الإضافات الأخرى أو حتّى القوالب إن أردت. المعلِّقات، الأفعال و المرشِّحات (Hooks, Actions, Filtres ) قبل البدء بإنشاء إضافة خاصّة، فلنبدأ أوّلًا بإلقاء نظرةٍ مفصّلةٍ على المعلِّقات "hooks"، الأفعال "actions" والمرشِّحات "filtres". إنّها تلك العناصر التي يترّكز عليها نظام الإضافة بالكامل والتّي ستُساعدك بشكلٍ كبيرٍ لفهم ما يحدث خلف الستار حول طريقة عمل الووردبريس. لنلقِ نظرة على هذا المثال. ماذا لو أردتَ إنشاء إضافةٍ تقوم بإضافة شفرة تتبُّع تحليليّةٍ لموقعك؟ في العادة هذا النوع من البرمجيّات يتمُّ إضافته في نهاية صفحة الموقع مباشرة فوق وسمِ الإغلاق. هذه الشفرة يجب أن تبقى في ذلك المكان حتّى وإن تمّ تغيير القالب ويجب أن تعمل على أيّ موقع ووردبريس، إذًا كيف ستجعل هذه الشفرة تعمل دائمًا إن لم تكن لديك أية سلطة تحكّم مباشر على القوالب؟ تذكّر أنّه في درس تطوير ووردبريس للمبتدئين: برمجة القوالب قمت بذكر دوال ()wp_head و ()wp_footer و الطريقة التي يجب عليك إضافتها بها في قالبك. مثالنا هذا يبيّن بالتحديد لماذا يجب أن توضع هذه الدّوال بالطريقة المحدّدة سلفًا. دالّة ()wp_footer يمكنها أن تقوم بتشغيل دوالٍّ أخرى محدّدة بواسطة الإضافات. إليك مثال كود يمكنك أن تستمتع بالتجريب عليه: <?php function my_tracking_code() { echo 'Paste tracking code from Google Analytics here'; } add_action( 'wp_footer', 'my_tracking_code' ); لنقم الآن بتفكيك الكود قطعةً قطعة. أوّلًا لقد قمت بكتابة دالّة تقوم بطبع شفرة التتبّع. هذه الدالّة غيرُ مستخدمة في أيّ مكان، هي فقط موجودة بإضافتي. بعد ذلك استخدمت دالّة ()add_action لإخبار الووردبريس متى يجب تشغيل الدالّة. متى ما وجد ووردبريس بدالّة ()wp_header فإنّه سيجد كلّ الدّوال المعلّقة بها بواسطة ()add_action . عندئذٍ سيقوم الووردبريس بتشغيل هذه الدّوال واحدة بواحدة وصولًا إلى دالّتنا التي تقوم بطبع شفرة التتبّع كنتيجة. هذا هو أساس المعلِّقات "hooks" ولكنّنا سنخوض فيها بشكلٍ أعمق لاحقًا خلال هذا الدَّرس لكي تتعلّم عنهم أكثر وتكون قادرًا على استغلال قدراتهم بشكل أفضل. قبل الانتقال إلى شيء آخر أريد ان ألفت انتباهك الى شيء مهم، دالة ()wp_footer الموجودة في تذييل القالب ليست معلِّقا "hook" بل هي في الحقيقة مجرّد دالّة تحتوي على معلِّق في مكان ما بداخلها. للنداء على معلِّق "hook" نستخدم (’do_action(’wp_footer، لكنّ هذا لم يكن مهمًّا في المثال الأوّل. على كلٍّ ستفهم أكثر عن هذا قريبًا! المصطلحات الآن وبعد أن أخذت لمحةً عمَّا سنتحدث عنه، دعني أشرح لك بطريقةٍ مرتّبةٍ المصطلحات التي ستلتقي بها طيلة هذا الدّرس. المعلِّقات "hooks" هي جزء من API إضافات الووردبريس. الأفعال والمرشِّحات "Actions and Filtres" عبارة عن نوع مختلف من المعلِّقات. الأفعال "actions" تسمح لك بإضافة وظائف، أنت في الأساس ستقوم بتحديد دالّة تشتغل في كلّ مرّة يقوم ووردبريس بمعالجة المعلِّق "hook" . المرشِّح "filter" يعمل بشكل مماثل و لكنّه عوض أن يضيف دوالًّا جديدة يقوم بتعديل الدوّال الموجودة مسبقًا بالووردبريس. مثلًا ووردبريس يحتوي على معلِّق يحدّد طول المقتطف إلى 55 كلمةٍ، يمكنك تعديل ذلك باستخدام المرشِّح لجعل عدد كلمات المقتطف أيَّ رقم ترغب به. كلًّ شيءٍ عن الأفعال: الآن وبعد أن صِرنا نعرف القواعد، فلنلقِ نظرةً مفصّلةً على دالّة ()add_action لفهمها بشكلٍ أفضل. هذه الدّالة تأخذ أربعة معاملات هم: اسم المعلِّق. اسم الدالّة التي نريد إضافتها للمعلِّق. الأولويّة. المعاملات المقبولة. لقد سبق وأن ألقينا نظرةً على أوَّل مُعاملين اللّذان هما في الحقيقة المُعاملان الوحيدان الاجباريّان بحيث يقومان بتحديد الدّالة المراد إضافتها والمكان المراد تشغيلها به. المُعامل الثالث يقوم بتحديد ترتيب الدوّال التي يتمّ تشغيلها. فائدة هذا المُعامل تظهر عندما تريد إضافة عدّة دوالٍّ إلى نفس المعلِّق. يمكنك مثلًا إضافة عدَّة شفرات تتبُّعٍ لموقعك وباستخدام مُعامل الأولوية يمكنك التحكُّم بأيّ دالّةٍ سيتمُّ تشغيلها أوّلًا. المُعامل الرّابع يخبر الووردبريس عدَد المعاملات التي تقبلها الدالّة المراد تعليقها. يجب عليك إلقاء نظرة في دليل الووردبريس لِمعرفة إن كان يتوّجب عليك وضع قيمة هنا لأنّ لكلِّ معلِّقِ خصائصه. بعض الدّوال قد تأخذ أكثر من معاملٍ، في هذه الحالة سيكون عليك أنّ تحدّد ذلك هنا. ألقِ نظرةً على المثال أدناه لكي تعرف لِما سيتوجّب عليك استخدام هذه الطريقة عند إنشاء معلِّقاتك الخاصّة لاحقًا. مثال عن فعل ووردبريس لا يوفّر الأفعال للقوالب فقط ولكن أيضًا لواجهة المدير إضافةً إلى أفعال خاصّة بالمستخدمين. معلِّق publish_post يتم تشغيله في كلِّ مرّةٍ يتم نشر مقال ما ليسمح لك بالقيام بشيءٍ ما في تلك المرحلة. الحاجة للمُعاملات تصبحُ ظاهرةً هنا. إذا قمتُ بإضافة دالّةٍ لهذا المعلِّق فكيف لي أن أعرف أيَّ مقالٍ تمَّ نشرهُ وماذا كانت تفاصيلهُ؟ عند قراءة دليل استخدام المعلِّقات يُمكنك أن ترى أنَّ الدالّة تحتوي على معاملين: مُعامِل معرِّف المقال ومُعامِل بيانات المقال. مثلًا لِنقم بإرسال بريدٍ إلكترونيٍّ لكاتب المقال عندما يُنشر مقاله: <?php function our_author_notification( $id, $post ) { $author = $post->post_author; $name = get_the_author_meta( 'display_name', $author ); $email = get_the_author_meta( 'user_email', $author ); $link = get_permalink( $id ); $message = 'Hello ' . $name . ", \n\n" . "Your artcile <a href='" . $link . "'>" . $post->post_title . "</a> has been published."; wp_mail( $email, 'One of your posts has been published', $message ); } add_action( 'publish_post', 'our_author_notification', 10, 2 ); كلًّ شيءٍ عن المرشِّحات: كما سبق وأن ذكرته سابقًا، المرشِّحات تعمل بشكلٍ مماثلٍ للأفعال ولكنّها تقوم بتعديل البيانات لذا ستجد نفسك تقوم بإرجاع نتائج داخل دوالّك المعلّقة. لإضافة مرشِّح نستخدم الدالّةَ ()add_filter التي ستستخدم نفس معاملات الدالّة ()add_action. مثال عن مرشِّح: أحسن مثالٍ للبدء به سيكون تعديل كلمة “قراءة المزيد” الموجودة بِنهاية المقتطف، في الوضع الافتراضي ستجدها بهذا الشَّكل "[…]". لتعديلها يمكنك القيام بشيء مثل هذا: <?php function our_excerpt_more( $more ) { return '... هناك المزيد'; } add_filter( 'excerpt_more', 'our_excerpt_more' ); لاحظ أنّني لم أحدّد مُعامل الأولويّة ولا مُعامل عدد المُعاملات المقبولة. هذا سيجعل الأولويّة 10 وعددُ المُعاملات المقبولة 1. هنا أنا لا أستخدم المعامل المُمرَّر في الدالة more$ إطلاقًا، أنا فقط أقوم بإرجاع نص خطّيٍ. يمكنك استخدام نفس الطريقة لتضمين إعلانٍ قبل الفقرة الأولى من مقالك. انظر الى المثال التالي: function in_content_ad( $content ) { $ad = '<div class="in-content-ad"><img src="https://placeholdit.imgix.net/~text?txtsize=33&txt=Advertisement&w=250&h=250"></div>'; return $ad . $content; } add_filter( 'the_content', 'in_content_ad' ); مع قليل من الـ CSS لتعويم الإعلان نحو اليمين، يُمكنك الحصول على صندوقِ إعلاناتٍ قياسيٍّ داخل محتوى مقالك خلال ثوانٍ معدودة. تعريف المعلِّقات: معرفة كيفيّة تعريف المعلِّقات إضافة إلى معرفةِ الطريقة التي تُنشِئ بها معلِّقاتك الخاصّة سيكون له أثرٌ كبير على فهمك لنظام ووردبريس. فلنفترض أنَّك تريد إنشاء إضافة متجر إلكتروني وأنّك ستقوم بإضافة وظائف لقائمة المنتجات، يمكنك القيام بشيءٍ مشابه لهذا: <?php $products = get_products( 10 ); show_products(); هنا أنت قمت بكتابةِ برمجيّتك بطريقة "إجباريّة" لكي تقوم بعرض فقط 10 منتجات. ولكن ماذا لو حاول شخص ما صنع إضافة لعملك وأراد أن يُظهر فقط ثلاثة منتجات؟ لو أنّك كتبك برمجيّتك بذكاء فإنّك ستسمح للأخرين بتعديلها. دعنا نستخدم معلّقًا للسماح للمطوّرين باستخدام عملنا وتطويره: <?php $product_count = apply_filters( 'product_count', 10 ); $products = get_products( $product_count ); show_products(); باستخدام دالّة ()apply_filters نحن نقوم بإخبار ووردبريس أنّ عندنا معلِّقًا اسمه product_count و أنّنا نريد أن نشغّل جميع الدّوال المربوطة به. في أيّة إضافة أخرى، يمكن للمطوّر أن يغيّر قيمة عدد المنتجات الظاهرة إلى ثلاثةٍ مثلًا باستخدام الطريقة التالية: <?php add_filter( 'product_count', 'carousel_product_count' ); function carousel_product_count( $count ) { return 3; } سنتابع في الدرس القادم أساسيات إنشاء الإضافة وكيفية إضافة خصائص القالب للإضافات… ترجمة -وبتصرّف- للمقال WordPress Development for Beginners: Building Plugins لصاحبه Daniel Pataki1 نقطة
-
تعرّفنا في الدرس السابق على المعلِّقات، الأفعال و المرشِّحات (Hooks, Actions, Filtres ) وسنتعرّف في هذا الدرس على أساسيات إنشاء الإضافة وكيفية إضافة خصائص القالب للإضافات… أساسيّات إنشاء إضافةٍ: الآن وقد صِرنا نعرف كلّ شيءٍ عن المعلِّقات فإن الخطوة التاليّة ستكون معرفة طريقة استخدامها. لنعد خطوة للوراء ولنلق نظرة عامّة على الإضافات. يتم وضع الإضافات في المجلّد الرئيسي للإضافات في موقعك ووردبريس، يوجد هذا الملف عادة في هذا المسار "wp-content/plugins". كمثال لنقم بإنشاء إضافة تقوم بإضافة رابط تغريده تحت مقالاتك. أوّلا، فلننشئ مجلّدًا في مجلّد الإضافات الرئيسي ولنسمّه مثلا "tweet-plugin-tutorial". الآن أنشئ ملفًّا داخل هذا المجلّد وسمّه "tweet-plugin-tutorial.php". الآن قم بفتح الملف وقم بنسخ البرمجيّة التالية بداخله: <?php /** * Plugin Name: Tweet Plugin Tutorial * Plugin URI: http://danielpataki.com * Description: This plugin adds a simple tweet link below posts. * Version: 1.0.0 * Author: Daniel Pataki * Author URI: http://danielpataki.com * License: GPL2 */ هذه المعلومات سيتم عرضها في لوحة تحكّم المدير في قسم الإضافات. بمجرّد حفظك للملف ستلاحظ ظهور إضافةٍ جديدةٍ في قائمة الإضافات، تهانينا! لقد أنشأت أوّل إضافة لك بإتقان ولكن لا تفرح كثيرًا فهذه الإضافة لا تقوم بأيّة وظيفة بعد، ولكن يمكنك تفعيلها الآن وهو ما ندعوك للقيام به. حاليًّا قمنا بـ 90% من العمل. كلُّ ما علينا القيام به الآن هو إيجاد طريقة لإضافة رابط التغريدة أسفل محتوى المقال. ربّما أنت تفكّر بتعديل محتوى المقال عن طريق إضافة نصٍّ إضافي له. هذا يعني أنّك تفكّر في استخدام معلِّق the_content مثلما قمنا بهذا سابقًا: <?php function tweet_link( $content ) { return $content . '<p><a href="https://twitter.com/intent/tweet?url='.get_permalink().'">Tweet about this</a></p>'; } add_action( 'the_content', 'tweet_link' ); هذه البرمجيّة ستقوم بإضافة رابطٍ بعد محتوى مقالِتنا، ما سيتيح لمستخدمينا مشاركة رابط المقال عن طريق تغريده. في الواقع هذا كلّ ما يجب عليك معرفته لإنشاء إضافة ووردبريس. جميع الإضافات هي عبارة عن مزيج من الدّوال والمعلِّقات التي تحدِّد المكان الذي سيتم منه تشغيل هذه الدّوال. لذا من الآن فصاعدًا عليك التركيز على تعلُّم الأنظمة الفرعية مثل Options API, Metadata API وغيرها. إضافة خصائص القالب للإضافات: هناك عدّة طرق لإضافة خصائص لقوالبك. دعنا أولًّا نلقي نظرة على الطريقة القياسية للقيام بهذا الأمر عن طريق دليل ووردبريس. سوف نبدأ بإنشاء صفحة مدير ستحتوي على النموذج التالي: <?php add_action('admin_menu', 'tweetlink_settings_menu'); function tweetlink_settings_menu() { add_menu_page('Tweet Link Settings', 'Tweet Link', 'manage_options', 'tweetlink-settings', 'tweetlink_settings_page', 'dashicons-twitter'); } function tweetlink_settings_page() { echo '<div class="wrap"><h2>Tweet Link Options</h2></div>'; } دالّة ()tweetlink_settings_menu تمّ تعليقها بمعلِّق admin_menu. حسب دليل الووردبريس هذه هي الطريقة الصحيحة التّي يجب استخدامها لإنشاء قائمة مدخلات في لوحة التحكّم. العمل سيتّم عن طريق دالّة ()add_menu_page التي تأخذ مجموعة مُعامِلات صفحة ذات مستوى أعلى. المعاملات التي تأخذها هذه الدّالة هي: عنوان الصّفحة – يستخدم في وسم العنوان عنوان القائمة – يستخدم في كتابة اسم القائمة في الجهة اليسرى/اليمنى من لوحة تحكم المدير. الإمكانيّة – لتحديد أقل مستوى يمكنه مشاهدة القائمة (مثلا القائمة ستكون ظاهرة فقط للمدير والمحرّرين) الاسم اللطيف للقائمة – يستخدم في الرابط المؤدّي لصفحة القائمة. الدّالة - اسم الدّالة التي تقوم بالتحكّم بالمُخرجات. الأيقونة – رابط صورة او نص لأيقونات ووردبريس. الموقع – تموضع العنصر ضمن القائمة. إليك كيف تبدو صفحة الخصائص في لوحة التحكم: قد تبدو لك هذه الصّفحة رائعة ولكن في الحقيقة هي مبالغةٌ كبيرة إنشاء صفحة كاملة من أجل إضافة بسيطة كالتّي نعمل عليها الآن، لذا سيكون من الأفضل وضع هذه الصفحة داخل قائمة الإعدادات الموجودة سلفا بالووردبريس. يمكنك القيام بذلك باستخدام دالّة ()add_options_page. الخطوة التّالية ستكون جعل الووردبريس يعلم حول إعدادات تغريدتنا. هذا الأمر سيتطلب استخدام دالّة ()register_settings داخل دالّة معلّقة إلى ()to admin_init. إليك كيف سيكون شكل البرمجيّة: <?php add_action( 'admin_init', 'tweetlink_settings' ); function tweetlink_settings() { register_setting( 'tweetlink_settings', 'twitter_account' ); } كلّ شيءٍ تمام. الخطوة الأخيرة ستكون إضافة استمارة تستخدم خصائصنا. استخدم البرمجيّة التاليّة كنموذج لإنشاء إعداداتك الخاصّة، لا تنسَ استخدام دوال ()settings_fields و ()do_settings_sections لضمان قيام الووردبريس بحفظ البيانات لك. <?php function tweetlink_settings_page() { ?> <div class="wrap"> <h2>Tweet Link Settings</h2> <form method="post" action="options.php"> <?php settings_fields( 'tweetlink_settings' ); ?> <?php do_settings_sections( 'tweetlink_settings' ); ?> <table class="form-table"> <tr valign="top"> <th scope="row">Twitter Account</th> <td><input type="text" name="twitter_account" value="<?php echo esc_attr( get_option('twitter_account') ); ?>" /></td> </tr> </table> <?php submit_button(); ?> </form> </div> <?php } عند نهاية العمليّة يجب أن ترى ظهور حقل جديد يُمكّنك من إدخال حسابك على تويتر ثم الضغط على حفظ. بعد ذلك يمكنك استخدام دالّة 'get_option( 'twitter_account ( لاسترجاع القيمة المُدخلة واستخدامها بأي مكان داخل إضافتك. يمكننا الآن التعديل على رابط التغريدة لإضافة "via @username" إذا قام المستخدمون بإضافة حسابهم على تويتر في صفحة الخصائص. إليك كيف عدّلت دالّة ()tweet_link <?php function tweet_link( $content ) { $url = 'https://twitter.com/intent/tweet'; $url .= '?url=' . get_permalink(); $account = get_option( 'twitter_account' ); if( !empty( $account ) ) { $url .= '&via=' . $account; } return $content . '<p><a href="' . $url . '">Tweet about this</a></p>'; } خاتمة هكذا نختم سلسلتنا حول تطوير ووردبريس للمبتدئين. في هذه المقالة تعلّمنا طريقة إنشاء إضافة بسيطة نتمنّى أنها خلقت داخلك شعورًا بالرّضى كونك تمكّنت من برمجة أوّل إضافة لك. فقط التجربة والإصرار من سيجعلان تطوير الووردبريس أسهل بالنسبة إليك وسترتقي من مجرّد مبتدئ غير خبير إلى مستوى مطوّر. ترجمة -وبتصرّف- للمقال WordPress Development for Beginners: Building Plugins لصاحبه Daniel Pataki1 نقطة