مقادیر پایه (Primitive Values) و ارجاعی (Reference Values)
در جاوا اسکریپت، متغیرها شامل دادههایی از نوع پایه و یا ارجاعی میباشند. مقادیر String ، Number ، Boolean ، Null و Undefined به عنوان مقادیر پایه محسوب میگردند. در این نوع متغیرها تغییرات، مستقیما بر روی مقدار ذخیره شده در متغیر اعمال میشوند. اشیاء نیز که از مجموعهای از مقادیر پایه ساخته شدهاند، مقادیر ارجاعی نامیده میشوند. در این نوع مقادیر، شما به اشارهگری از شیء دسترسی دارید. بنابراین تغییرات مستقیما بر روی دادههای ذخیره شده اعمال نمیشوند. به مثالهای زیر توجه کنید:
var num1 = 10; var num2 = num1; alert("Num1=" + num1 + ", Num2=" + num2); num2 = 20; alert("Num1=" + num1 + ", Num2=" + num2); num1 = 30; alert("Num1=" + num1 + ", Num2=" + num2);
خروجی :
"Num1=10, Num2=10"
"Num1=10, Num2=20"
"Num1=30, Num2=20"
همانطور که از خروجی مثال فوق پیداست، در انتساب num1 به num2 ، مقدار num1 در num2 کپی شدهاست. بنابراین تغییراتی که بر روی num1 یا num2 صورت میگیرد، مستقیما بر روی مقدار ذخیره شده در هر یک از این متغیرها تاثیر میگذارد. رفتار مقادیر پایه همیشه به همین صورت میباشد.
var obj1 = new Object(); obj1.num = 10; var obj2 = obj1; alert("Obj1.Num=" + obj1.num + ", Obj2.Num=" + obj2.num); obj2.num = 20; alert("Obj1.Num=" + obj1.num + ", Obj2.Num=" + obj2.num); obj1.num = 30; alert("Obj1.Num=" + obj1.num + ", Obj2.Num=" + obj2.num);
خروجی :
"Obj1.Num=10, Obj2.Num=10"
"Obj1.Num=20, Obj2.Num=20"
"Obj1.Num=30, Obj2.Num=30"
با استفاده از نوع ارجاعی Object میتوانیم اشیاء جدیدی را ایجاد کنیم و ویژگیهایی را به صورت پویا به آنها اختصاص دهیم. همانطور که قبلا گفته شد، اشیاء از نوع ارجاعی میباشند و حاوی اشارهگری به مقادیر ذخیره شده میباشند. بنابراین انتساب obj1 به obj2 به معنای انتساب اشارهگر obj1 به obj2 میباشد. به عبارتی دیگر obj2 به همانجایی اشاره میکند که obj1 نیز اشاره مینماید. پس هر تغییری که بر روی ویژگیهای obj1 رخ دهد، obj2 نیز تاثیرات آن را میبیند و بالعکس. همانطور که در خروجی مشاهده مینمایید، در مرحلهی اول obj1 به obj2 نسبت داده شد، پس مقدار ویژگی num برای هر دو آنها یکسان میباشد. در مرحلهی دوم، مقدار ویژگی num را در obj2 تغییر دادیم؛ ولی مقدار این ویژگی، در obj1 نیز تغییر نمود. در مرحلهی سوم نیز همان اتفاقات مرحلهی دوم تکرار شد.
با توجه به مثالهای فوق قطعا به تفاوتهای مقادیر پایه و ارجاعی پی بردید. همچنین در یک نمونهی کوچک و ساده نیز، یکی از روشهای ایجاد شیء را که استفاده از نوع ارجاعی Object میباشد، مشاهده نمودید. این دانستهها مقدمه ای بر شروع برنامه نویسی شیء گرا میباشند. ولی قبل از شروع برنامه نویسی شیء گرا در جاوا اسکریپت، به بررسی نکات و تکنیکهای دیگری میپردازیم.
توجه:
به انواع پایه، انواع دادهای مقداری یا اولیه نیز گفته میشود.
فراخوانی با مقدار (Call by Value)
در این نوع فراخوانی، آرگومانها از نوع مقادیر پایه هستند. بنابراین هر تغییری که بر روی آرگومانها در تابع رخ دهد، هیچ تاثیری بر روی آرگومانهای ارسالی در زمان فراخوانی تابع ندارند. به مثال زیر توجه کنید:
function primitive(a, b) { a += 100; b += 200; alert("a=" + a + ", b=" + b); } var x = 300, y = 400; primitive(x, y); alert("x=" + x + ", y=" + y);
خروجی :
"a=400, b=600"
"x=300, y=400"
x و y دو متغیر پایه میباشند، بنابراین تابع فوق به صورت مقداری فراخوانی شدهاست. یعنی مقدار آرگومانهای x و y در آرگومانهای a و b کپی میشوند. پس هر تغییری که بر روی a و b رخ دهد، هیچ تاثیری بر روی x و y ندارد. همچنین با توجه به توضیحی که در مورد مقادیر پایه داده شد، تغییرات مستقیما بر روی دادهی ذخیره شده در متغیر اعمال میشود. بنابراین تغییراتی که در تابع فوق بر روی a و b رخ داد، مستقیما مقادیر a و b را تغییر دادهاست وهیچ ارتباطی به x و y ندارد. البته توجه داشته باشید که حتی اگر نام آرگومانهای تابع با آرگومانهای ارسالی یکسان بود و یا حتی اگر تابع مقداری را به عنوان خروجی بر میگرداند، هیچ تفاوتی را در خروجی برنامه فوق مشاهده نمیکردید.
فراخوانی با ارجاع (Call by Reference)
در این نوع فراخوانی، آرگومانها از نوع مقادیر ارجاعی هستند. بنابراین هر تغییری که بر روی آرگومانها در تابع رخ دهد، بر روی آرگومانهای ارسالی در زمان فراخوانی تابع نیز تاثیر میگذارند. به مثال زیر توجه کنید:
function reference(obj) { obj.a += 100; obj.b += 200; alert("obj.a=" + obj.a + ", obj.b=" + obj.b); } var calc = new Object(); calc.a = 300; calc.b = 400; reference(calc); alert("calc.a=" + calc.a + ", calc.b=" + calc.b);
خروجی :
"obj.a=400, obj.b=600"
"calc.a=400, calc.b=600"
calc یک مقدار ارجاعی است که به عنوان آرگومان ورودی به تابع ارسال میشود و اشارهگر خود را به obj اختصاص میدهد. بنابراین obj به همان آدرسی اشاره میکند که calc اشاره مینماید. پس هر تغییری که بر روی obj رخ دهد، calc نیز تاثیرات آن را مشاهده مینماید. همانطور که در خروجی نیز مشاهده مینمایید، تغییرات صورت گرفته در تابع به calc نیز منعکس شده است.
حوزه دسترسی به متغیرها (Variable Scope)
متغیرهای محلی (Local Variables)
در اکثر زبانهای برنامه نویسی، متغیرهایی که در یک بلاک کد تعریف میشوند، به صورت محلی و فقط در همان بلاک کد قابل دسترسی میباشند. به این متغیرها، متغیرهای محلی میگویند که با خاتمهی اجرای بلاک کد، از بین خواهند رفت و دیگر قابل دسترسی نمیباشند. اما در جاوا اسکریپت حوزهی اجرایی متغیرها کمی متفاوت میباشد. به مثال زیر توجه کنید:
for (var i = 1; i <= 5; i++) { var sqr = i * i; alert(sqr); } alert(i); alert(sqr);
خروجی :
متغیرهای i و sqr داخل حلقهی for تعریف شدهاند و منطقا نباید خارج از این حلقه قابل دسترسی باشند. ولی با توجه به خروجی فوق، مشاهده نمودید که متغیرهای i و sqr، نه تنها خارج از این حلقه قابل شناسایی میباشند، بلکه آخرین مقدار خود را نیز حفظ نمودهاند. در جاوا اسکریپت، یک متغیر محلی زمانی مفهوم پیدا میکند که در داخل یک تابع تعریف شود. به مثال زیر توجه کنید:
function sqr(num) { var sum = num * num; return sum; } var n = 4; alert(sqr(n)); alert(num); // Error: num is not defined alert(sum); // Error: sum is not defined
خروجی :
16
Error: num is not defined
Error: sum is not defined
همانطور که مشاهده میکنید، متغیرهای num و sum به صورت محلی در تابع فوق تعریف شدهاند؛ بنابراین خارج از تابع قابل دسترسی نمیباشند و موجب بروز خطا میگردند.
متغیرهای عمومی (Global Variables)
در جاوا اسکریپت، متغیرهایی که خارج از تابع تعریف میگردند، به شیء window نسبت داده میشوند و عمومی میباشند. به عبارتی دیگر این متغیرها به عنوان یک ویژگی از شیء window تعریف میشوند و در تمامی توابع و بلاکهای کد قابل دسترسی میباشند. به مثال زیر توجه کنید:
var color = "Red"; function setColor() { color = "Blue"; } alert(color); setColor(); alert(color);
خروجی :
"Red"
"Blue"
در مثال فوق، متغیر color به صورت عمومی تعریف شدهاست. بنابراین در کل برنامه قابل دسترسی میباشد. در alert اول مقدار فعلی متغیر color یعنی “Red” نمایش مییابد. سپس با فراخوانی تابع، مقدار این متغیر تغییر میکند. در alert دوم مقدار تغییر یافتهی متغیر color نمایش خواهد یافت. حال به مثال زیر توجه کنید:
var color = "Red"; function getColor() { var color = "Blue"; return color; } alert(color); alert(getColor()); alert(color);
خروجی :
"Red"
"Blue"
"Red"
در مثال فوق، ابتدا یک متغیر color به صورت عمومی یا Global تعریف شده است. در تابع getColor نیز یک متغیر color به صورت local یا محلی تعریف شده است. زمانی که در alert تابع getColor فراخوانی میشود، متغیر color مقداردهی میگردد. این مقداردهی برای متغیر محلی صورت گرفته است و هیچ ربطی به متغیر color که به صورت عمومی تعریف شده است ندارد.
جهت تعریف متغیر در جاوا اسکریپت، از کلمهی کلیدی var استفاده میشود. اما تعریف متغیر در جاوا اسکریپت اجباری نمیباشد و میتوان یک متغیر را مقداردهی نمود بدون آنکه تعریف شده باشد. در صورتی که متغیر با var اعلان نشود، آن متغیر به شیء window نسبت داده میشود و ماهیت عمومی پیدا میکند. به مثال زیر توجه کنید:
function sum(a, b) { c = a + b; } sum(20, 30); alert(c);
خروجی :
50
همانطور که مشاهده میکنید، متغیر c بدون تعریف شدن مورد استفاده قرار گرفته است. با اینکه به صورت محلی مقداردهی گردیده است، ولی چون توسط var اعلان نشده است، به شیء window نسبت داده شده و ماهیت عمومی پیدا کرده است.
کامپوننتهای jQuery زیادی وجود دارند که توسط آنها میتوان تصاویر را بصورت زمانبندی شده و به همراه افکتهای زیبا در سایت خود نشان داد. مانند اینجا .
در این قصد ایجاد helper برای کامپوننت NivoSlider را داریم.
1- یک پروژه Asp.net Mvc 4.0 ایجاد میکنیم.
2- سپس فایل jquery.nivo.slider.pack.js ، فایلهای css مربوط به این کامپوننت و چهار تم موجود را از سایت این کامپوننت و یا درون سورس مثال ارائه شده دریافت میکنیم.
3- به کلاس BundleConfig رفته و کدهای زیر را اضافه میکنیم:
#region Nivo Slider bundles.Add(new StyleBundle("~/Content/NivoSlider").Include("~/Content/nivoSlider/nivo-slider.css")); bundles.Add(new StyleBundle("~/Content/NivoSliderDefaultTheme").Include("~/Content/nivoSlider/themes/default/default.css")); bundles.Add(new StyleBundle("~/Content/NivoSliderDarkTheme").Include("~/Content/nivoSlider/themes/dark/dark.css")); bundles.Add(new StyleBundle("~/Content/NivoSliderLightTheme").Include("~/Content/nivoSlider/themes/light/light.css")); bundles.Add(new StyleBundle("~/Content/NivoSliderBarTheme").Include("~/Content/nivoSlider/themes/bar/bar.css")); bundles.Add(new ScriptBundle("~/bundles/NivoSlider").Include("~/Scripts/jquery.nivo.slider.pack.js")); #endregion
سپس در فایل shared آنها را بصورت زیر اعمال میکنیم:
<!DOCTYPE html> <html> <head> <title>@ViewBag.Title</title> <script type="text/javascript" src="~/Scripts/jquery-1.9.1.min.js"></script> @Styles.Render("~/Content/NivoSlider", "~/Content/NivoSliderDefaultTheme","~/Content/NivoSliderLightTheme") @Scripts.Render("~/bundles/NivoSlider") </head> <body> @RenderBody() @RenderSection("scripts", required: false) </body> </html>
4- یک پوشه با عنوان helper به پروژه اضافه میکنیم. سپس کلاسهای زیر را به آن اضافه میکنیم :
public class NivoSliderHelper { #region Fields private string _id = "nivo1"; private List<NivoSliderItem> _models = null; private string _width = "100%"; private NivoSliderTheme _theme = NivoSliderTheme.Default; private string _effect = "random"; // Specify sets like= 'fold;fade;sliceDown' private int _slices = 15; // For slice animations private int _boxCols = 8; // For box animations private int _boxRows = 4; // For box animations private int _animSpeed = 500; // Slide transition speed private int _pauseTime = 3000; // How long each slide will show private int _startSlide = 0; // Set starting Slide (0 index) private bool _directionNav = true; // Next & Prev navigation private bool _controlNav = true; // 1;2;3... navigation private bool _controlNavThumbs = false; // Use thumbnails for Control Nav private bool _pauseOnHover = true; // Stop animation while hovering private bool _manualAdvance = false; // Force manual transitions private string _prevText = "Prev"; // Prev directionNav text private string _nextText = "Next"; // Next directionNav text private bool _randomStart = false; // Start on a random slide private string _beforeChange = ""; // Triggers before a slide transition private string _afterChange = ""; // Triggers after a slide transition private string _slideshowEnd = ""; // Triggers after all slides have been shown private string _lastSlide = ""; // Triggers when last slide is shown private string _afterLoad = ""; #endregion string makeParameters() { var builder = new StringBuilder(); builder.Append("{"); builder.Append(string.Format("effect:'{0}'", _effect)); builder.AppendLine(string.Format(",slices:{0}", _slices)); builder.AppendLine(string.Format(",boxCols:{0}", _boxCols)); builder.AppendLine(string.Format(",boxRows:{0}", _boxRows)); builder.AppendLine(string.Format(",animSpeed:{0}", _animSpeed)); builder.AppendLine(string.Format(",pauseTime:{0}", _pauseTime)); builder.AppendLine(string.Format(",startSlide:{0}", _startSlide)); builder.AppendLine(string.Format(",directionNav:{0}", _directionNav.ToString().ToLower())); builder.AppendLine(string.Format(",controlNav:{0}", _controlNav.ToString().ToLower())); builder.AppendLine(string.Format(",controlNavThumbs:{0}", _controlNavThumbs.ToString().ToLower())); builder.AppendLine(string.Format(",pauseOnHover:{0}", _pauseOnHover.ToString().ToLower())); builder.AppendLine(string.Format(",manualAdvance:{0}", _manualAdvance.ToString().ToLower())); builder.AppendLine(string.Format(",prevText:'{0}'", _prevText)); builder.AppendLine(string.Format(",nextText:'{0}'", _nextText)); builder.AppendLine(string.Format(",randomStart:{0}", _randomStart.ToString().ToLower())); builder.AppendLine(string.Format(",beforeChange:{0}", _beforeChange)); builder.AppendLine(string.Format(",afterChange:{0}", _afterChange)); builder.AppendLine(string.Format(",slideshowEnd:{0}", _slideshowEnd)); builder.AppendLine(string.Format(",lastSlide:{0}", _lastSlide)); builder.AppendLine(string.Format(",afterLoad:{0}", _afterLoad)); builder.Append("}"); return builder.ToString(); } public NivoSliderHelper ( string id, List<NivoSliderItem> models, string width = "100%", NivoSliderTheme theme = NivoSliderTheme.Default, string effect = "random", int slices = 15, int boxCols = 8, int boxRows = 4, int animSpeed = 500, int pauseTime = 3000, int startSlide = 0, bool directionNav = true, bool controlNav = true, bool controlNavThumbs = false, bool pauseOnHover = true, bool manualAdvance = false, string prevText = "Prev", string nextText = "Next", bool randomStart = false, string beforeChange = "function(){}", string afterChange = "function(){}", string slideshowEnd = "function(){}", string lastSlide = "function(){}", string afterLoad = "function(){}") { _id = id; _models = models; _width = width; _theme = theme; _effect = effect; _slices = slices; _boxCols = boxCols; _boxRows = boxRows; _animSpeed = animSpeed; _pauseTime = pauseTime; _startSlide = startSlide; _directionNav = directionNav; _controlNav = controlNav; _controlNavThumbs = controlNavThumbs; _pauseOnHover = pauseOnHover; _manualAdvance = manualAdvance; _prevText = prevText; _nextText = nextText; _randomStart = randomStart; _beforeChange = beforeChange; _afterChange = afterChange; _slideshowEnd = slideshowEnd; _lastSlide = lastSlide; _afterLoad = afterLoad; } public IHtmlString GetHtml() { var thm = "theme-" + _theme.ToString().ToLower(); var sb = new StringBuilder(); sb.AppendLine("<div style='width:" + _width + ";height:auto;margin:0px auto' class='" + thm + "'>"); sb.AppendLine("<div id='" + _id + "' class='nivoSlider'>"); foreach (var model in _models) { string img = string.Format("<img src='{0}' alt='{1}' title='{2}' />", model.ImageFilePath, "", model.Caption); string item = ""; if (model.LinkUrl.Trim().Length > 0 && Uri.IsWellFormedUriString(model.LinkUrl, UriKind.RelativeOrAbsolute)) { item = string.Format("<a href='{0}'>{1}</a>", model.LinkUrl, img); } else { item = img; } sb.AppendLine(item); } sb.AppendLine("</div>"); sb.AppendLine("</div>"); sb.AppendLine("<script type='text/javascript'>"); //sb.AppendLine("$('#" + _id + "').parent().ready(function () {"); sb.Append("$(document).ready(function(){"); sb.AppendLine("$('#" + _id + "').nivoSlider(" + makeParameters() + ");"); //sb.AppendLine("$('.nivo-controlNav a').empty();");//semi hack for rtl layout //sb.AppendLine("$('#" + _id + "').nivoSlider();"); sb.AppendLine("});"); sb.AppendLine("</script>"); return new HtmlString(sb.ToString()); } }
public enum NivoSliderTheme { Default, Light, Dark, Bar, }
public class NivoSliderItem { public string ImageFilePath { get; set; } public string Caption { get; set; } public string LinkUrl { get; set; } }
public class HomeController : Controller { public ActionResult Index() { return View(); } public virtual ActionResult NivoSlider() { var models = new List<NivoSliderItem> { new NivoSliderItem() { ImageFilePath =Url.Content("~/Images/img1.jpg"), Caption = "عنوان اول", LinkUrl = "http://www.google.com", }, new NivoSliderItem() { ImageFilePath =Url.Content("~/Images/img2.jpg"), Caption = "#htmlcaption", LinkUrl = "", }, new NivoSliderItem() { ImageFilePath =Url.Content("~/Images/img3.jpg"), Caption = "عنوان سوم", LinkUrl = "", }, new NivoSliderItem() { ImageFilePath =Url.Content("~/Images/img4.jpg"), Caption = "عنوان چهارم", LinkUrl = "", }, new NivoSliderItem() { ImageFilePath =Url.Content("~/Images/img5.jpg"), Caption = "عنوان پنجم", LinkUrl = "", } }; return PartialView("_NivoSlider", models); } }
@{ ViewBag.Title = "Index"; } <h2>Nivo Slider Index</h2> @{ Html.RenderAction("NivoSlider", "Home");}
@using NivoSlider.Helper @model List<NivoSliderItem> @{ var nivo1 = new NivoSliderHelper("nivo1", Model.Skip(0).Take(3).ToList(), theme: NivoSliderTheme.Default, width: "500px"); var nivo2 = new NivoSliderHelper("nivo2", Model.Skip(3).Take(2).ToList(), theme: NivoSliderTheme.Light, width: "500px"); } @nivo1.GetHtml() <div id="htmlcaption"> <strong>This</strong> is an example of a <em>HTML</em> caption with <a href="#">a link</a>. </div> <br /> <br /> @nivo2.GetHtml()
مراحل کار به این صورت هستند:
بارگذاری تصویر و چرخش آن در صورت نیاز
ابتدا تصویر بارکد دار را بارگذاری کرده و آنرا تبدیل به یک تصویر سیاه و سفید میکنیم:
// load the image and convert it to grayscale var image = new Mat(fileName); if (rotation != 0) { rotateImage(image, image, rotation, 1); } if (debug) { Cv2.ImShow("Source", image); Cv2.WaitKey(1); // do events } var gray = new Mat(); var channels = image.Channels(); if (channels > 1) { Cv2.CvtColor(image, gray, ColorConversion.BgrToGray); } else { image.CopyTo(gray); }
تشخیص گرادیانهای افقی و عمودی
یکی از روشهای تشخیص بارکد، استفاده از روشی است که در تشخیص خودرو قسمت 16 بیان شد. تعداد زیادی تصویر بارکد را تهیه و سپس آنها را به الگوریتمهای machine learning جهت تشخیص و یافتن محدودهی بارکد موجود در یک تصویر، ارسال کنیم. هرچند این روش جواب خواهد داد، اما در این مورد خاص، قسمت بارکد، شبیه به گرادیانی از رنگها است. کتابخانهی OpenCV برای یافتن این نوع گرادیانها دارای متدی است به نام Sobel :
// compute the Scharr gradient magnitude representation of the images // in both the x and y direction var gradX = new Mat(); Cv2.Sobel(gray, gradX, MatType.CV_32F, xorder: 1, yorder: 0, ksize: -1); //Cv2.Scharr(gray, gradX, MatType.CV_32F, xorder: 1, yorder: 0); var gradY = new Mat(); Cv2.Sobel(gray, gradY, MatType.CV_32F, xorder: 0, yorder: 1, ksize: -1); //Cv2.Scharr(gray, gradY, MatType.CV_32F, xorder: 0, yorder: 1); // subtract the y-gradient from the x-gradient var gradient = new Mat(); Cv2.Subtract(gradX, gradY, gradient); Cv2.ConvertScaleAbs(gradient, gradient); if (debug) { Cv2.ImShow("Gradient", gradient); Cv2.WaitKey(1); // do events }
ابتدا درجهی شدت گرادیانها در جهتهای x و y محاسبه میشوند. سپس این شدتها از هم کم خواهند شد تا بیشترین شدت گرادیان موجود در محور x حاصل شود. این بیشترین شدتها، بیانگر نواحی خواهند بود که احتمال وجود بارکدهای افقی در آنها بیشتر است.
کاهش نویز و یکی کردن نواحی تشخیص داده شده
در ادامه میخواهیم با استفاده از متدهای تشخیص کانتور (قسمت 12)، نواحی با بیشترین شدت گرادیان افقی را پیدا کنیم. اما تصویر حاصل از قسمت قبل برای اینکار مناسب نیست. به همین جهت با استفاده از متدهای کار با مورفولوژی تصاویر، این نواحی گرادیانی را یکی میکنیم (قسمت 8).
// blur and threshold the image var blurred = new Mat(); Cv2.Blur(gradient, blurred, new Size(9, 9)); var threshImage = new Mat(); Cv2.Threshold(blurred, threshImage, thresh, 255, ThresholdType.Binary); if (debug) { Cv2.ImShow("Thresh", threshImage); Cv2.WaitKey(1); // do events } // construct a closing kernel and apply it to the thresholded image var kernel = Cv2.GetStructuringElement(StructuringElementShape.Rect, new Size(21, 7)); var closed = new Mat(); Cv2.MorphologyEx(threshImage, closed, MorphologyOperation.Close, kernel); if (debug) { Cv2.ImShow("Closed", closed); Cv2.WaitKey(1); // do events } // perform a series of erosions and dilations Cv2.Erode(closed, closed, null, iterations: 4); Cv2.Dilate(closed, closed, null, iterations: 4); if (debug) { Cv2.ImShow("Erode & Dilate", closed); Cv2.WaitKey(1); // do events }
ابتدا با استفاده از متد Threshold، تصویر را به یک تصویر باینری تبدیل خواهیم کرد. در این تصویر تمام نقاط دارای شدت رنگ کمتر از مقدار thresh، به مقدار حداکثر 255 تنظیم میشوند.
سپس با استفاده از متدهای تغییر مورفولوژی تصویر، قسمتهای مجاور به هم را میبندیم و یکی میکنیم. این مورد در یافتن اشیاء احتمالی که ممکن است بارکد باشند، بسیار مفید است.
متدهای Erode و Dilate در اینجا کار حذف نویزهای اضافی را انجام میدهند؛ تا بهتر بتوان بر روی نواحی بزرگتر یافت شده، تمرکز کرد.
یافتن بزرگترین ناحیهی به هم پیوستهی موجود در یک تصویر
تمام این مراحل را انجام دادیم تا بتوانیم بزرگترین ناحیهی به هم پیوستهای را که احتمال میرود بارکد باشد، در تصویر تشخیص دهیم. پس از این آماده سازیها، اکنون با استفاده از متد یافتن کانتورها، تمام نواحی یکی شده را یافته و بزرگترین مساحت ممکن را به عنوان بارکد انتخاب میکنیم:
//find the contours in the thresholded image, then sort the contours //by their area, keeping only the largest one Point[][] contours; HiearchyIndex[] hierarchyIndexes; Cv2.FindContours( closed, out contours, out hierarchyIndexes, mode: ContourRetrieval.CComp, method: ContourChain.ApproxSimple); if (contours.Length == 0) { throw new NotSupportedException("Couldn't find any object in the image."); } var contourIndex = 0; var previousArea = 0; var biggestContourRect = Cv2.BoundingRect(contours[0]); while ((contourIndex >= 0)) { var contour = contours[contourIndex]; var boundingRect = Cv2.BoundingRect(contour); //Find bounding rect for each contour var boundingRectArea = boundingRect.Width * boundingRect.Height; if (boundingRectArea > previousArea) { biggestContourRect = boundingRect; previousArea = boundingRectArea; } contourIndex = hierarchyIndexes[contourIndex].Next; } var barcode = new Mat(image, biggestContourRect); //Crop the image Cv2.CvtColor(barcode, barcode, ColorConversion.BgrToGray); Cv2.ImShow("Barcode", barcode); Cv2.WaitKey(1); // do events
خواندن مقدار متناظر با بارکد یافت شده
خوب، تا اینجا موفق شدیم، محل قرارگیری بارکد را تصویر پیدا کنیم. مرحلهی بعد خواندن مقدار متناظر با این تصویر است. برای این منظور از کتابخانهی سورس بازی به نام http://zxingnet.codeplex.com استفاده خواهیم کرد. این کتابخانه قادر است بارکد بسازد و همچنین تصاویر بارکدها را خوانده و مقادیر متناظر با آنها را استخراج کند. برای نصب آن میتوان از دستور ذیل استفاده کرد:
PM> Install-Package ZXing.Net
private static string getBarcodeText(Mat barcode) { // `ZXing.Net` needs a white space around the barcode var barcodeWithWhiteSpace = new Mat(new Size(barcode.Width + 30, barcode.Height + 30), MatType.CV_8U, Scalar.White); var drawingRect = new Rect(new Point(15, 15), new Size(barcode.Width, barcode.Height)); var roi = barcodeWithWhiteSpace[drawingRect]; barcode.CopyTo(roi); Cv2.ImShow("Enhanced Barcode", barcodeWithWhiteSpace); Cv2.WaitKey(1); // do events return decodeBarcodeText(barcodeWithWhiteSpace.ToBitmap()); } private static string decodeBarcodeText(System.Drawing.Bitmap barcodeBitmap) { var source = new BitmapLuminanceSource(barcodeBitmap); // using http://zxingnet.codeplex.com/ // PM> Install-Package ZXing.Net var reader = new BarcodeReader(null, null, ls => new GlobalHistogramBinarizer(ls)) { AutoRotate = true, TryInverted = true, Options = new DecodingOptions { TryHarder = true, //PureBarcode = true, /*PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.CODE_128 //BarcodeFormat.EAN_8, //BarcodeFormat.CODE_39, //BarcodeFormat.UPC_A }*/ } }; //var newhint = new KeyValuePair<DecodeHintType, object>(DecodeHintType.ALLOWED_EAN_EXTENSIONS, new Object()); //reader.Options.Hints.Add(newhint); var result = reader.Decode(source); if (result == null) { Console.WriteLine("Decode failed."); return string.Empty; } Console.WriteLine("BarcodeFormat: {0}", result.BarcodeFormat); Console.WriteLine("Result: {0}", result.Text); var writer = new BarcodeWriter { Format = result.BarcodeFormat, Options = { Width = 200, Height = 50, Margin = 4}, Renderer = new ZXing.Rendering.BitmapRenderer() }; var barcodeImage = writer.Write(result.Text); Cv2.ImShow("BarcodeWriter", barcodeImage.ToMat()); return result.Text; }
الف) این کتابخانه حتما نیاز دارد تا تصویر بارکد، در یک حاشیهی سفید در اختیار او قرار گیرد. به همین جهت در متد getBarcodeText، ابتدا تصویر بارکد یافت شده، به میانهی یک مستطیل سفید رنگ بزرگتر کپی میشود.
ب) برای تبدیل Mat به Bitmap مورد نیاز این کتابخانه میتوان از متد الحاقی ToBitmap استفاده کرد (قسمت 7).
ج) پس از آن وهلهای از کلاس BarcodeReader آماده شده و در آن پارامترهایی مانند بیشتر سعی کن (TryHarder) و اصلاح درجهی چرخش تصویر (AutoRotate) تنظیم شدهاند.
د) بارکدهای موجود در قبضهای ایران عموما بر اساس فرمت CODE_128 ساخته میشوند. بنابراین برای خواندن سریعتر آنها میتوان PossibleFormats را مقدار دهی کرد. اگر این مقدار دهی صورت نگیرد، تمام حالتهای ممکن بررسی میشوند.
در آخر کار این متد، از متد Writer آن نیز برای تولید بارکد مشابهی استفاده شدهاست تا بتوان بررسی کرد این دو تا چه اندازه به هم شبیه هستند.
همانطور که مشاهده میکنید، عدد تشخیص داده شده، با عدد شناسهی قبض و شناسهی پرداخت تصویر ابتدای بحث یکی است.
بهبود تصویر، پیش از ارسال آن به متد Decode کتابخانهی ZXing.Net
در تصویر قبلی، سطر decode failed را هم ملاحظه میکنید. علت اینجا است که اولین سعی انجام شده، موفق نبوده است؛ چون تصویر تشخیص داده شده، بیش از اندازه نویز و حاشیهی خاکستری دارد. میتوان این حاشیهی خاکستری را با دوبار اعمال متد Threshold از بین برد:
var barcodeClone = barcode.Clone(); var barcodeText = getBarcodeText(barcodeClone); if (string.IsNullOrWhiteSpace(barcodeText)) { Console.WriteLine("Enhancing the barcode..."); //Cv2.AdaptiveThreshold(barcode, barcode, 255, //AdaptiveThresholdType.GaussianC, ThresholdType.Binary, 9, 1); //var th = 119; var th = 100; Cv2.Threshold(barcode, barcode, th, 255, ThresholdType.ToZero); Cv2.Threshold(barcode, barcode, th, 255, ThresholdType.Binary); barcodeText = getBarcodeText(barcode); } Cv2.Rectangle(image, new Point(biggestContourRect.X, biggestContourRect.Y), new Point(biggestContourRect.X + biggestContourRect.Width, biggestContourRect.Y + biggestContourRect.Height), new Scalar(0, 255, 0), 2); if (debug) { Cv2.ImShow("Segmented Source", image); Cv2.WaitKey(1); // do events } Cv2.WaitKey(0); Cv2.DestroyAllWindows();
اعداد یافت شده، دقیقا از روی تصویر بهبود یافتهی توسط متدهای Threshold خوانده شدهاند و نه تصویر ابتدایی یافت شده. بنابراین به این موضوع نیز باید دقت داشت.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
نحوه ساخت Pipe سفارشی
علاوه بر استفاده از Pipeهای از پیش ساخته شده Angular، شما میتوانید Pipeهای سفارشی خود را نیز تعریف و استفاده کنید. به عنوان مثال میخواهیم Pipe ای را با نام perNumber تعریف کنیم، تا تمامی اعداد موجود در عبارت ورودی Pipe را به صورت اعداد فارسی نمایش دهد. یعنی با اعمال این Pipe به عدد 123456 خروجی ۱۲۳۴۵۶ مورد انتظار است. برای ایجاد Pipe سفارشی مراحل زیر را انجام دهید.
قدم اول - ساخت یک فایل با نام دلخواه
طبق Style Guide در Angular.io نام این فایل را per-number.pipe.ts انتخاب میکنیم.
قدم دوم – افزودن ماژولهای مورد نیاز
داخل فایل ایجاد شده ماژولهای Pipe و PipeTransform را با استفاده از دستور import از angular/core@ اضافه میکنیم.
import { Pipe, PipeTransform } from '@angular/core';
قدم سوم – ساخت کلاس و مزین کردن آن به Pipe@
یک کلاس با نام دلخواه را مثلا به نام PerNumberPipe ایجاد میکنیم. این کلاس علاوه بر اینکه PipeTransform را پیاده سازی خواهد کرد، مزین به متادیتای Pipe@ نیز میباشد. متادیتای Pipe@ هنگام تزئین کلاس، یک نام را دریافت میکند. این نام قرار است به عنوان نام نهایی Pipe برای اعمال بر روی Template expressions مورد استفاده قرار بگیرد.
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'perNumber'}) export class PerNumberPipe implements PipeTransform { }
قدم چهارم – پیاده سازی متد transform
به واسطه اعمال اینترفیس PipeTransform، این کلاس باید متد transform را پیاده سازی کند. این متد در پارامتر اول خود، عبارت ورودی را که قرار است Pipe بر روی آن اعمال شود، دریافت میکند و در ادامه تعداد دلخواهی پارامتر ورودی Pipe را که میخواهد، میتواند دریافت کند.
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'perNumber'}) export class PerNumberPipe implements PipeTransform { transform(value: any, ...args: any[]): any { } }
نکته ۱: نام انتخابی برای Pipe در آذینگر Pipe@ بایستی یک شناسه معتبر در JavaScript باشد.
نکته ۲: متد transform برای Pipe اجباری است و حتما بایستی پیاده سازی شود. اینترفیس PipeTransform این متد را برای کلاس اجباری میکند؛ هرچند استفاده از این اینترفیس برای کلاس Pipe کاملا اختیاری است.
قدم آخر – نوشتن کد تبدیل اعداد
Pipe مورد نظر ما قرار است یک رشته عددی را از ورودی دریافت کند و تمامی اعداد لاتین آن را به فارسی تبدیل کند. همچنین این Pipe هیچگونه پارامتری را دریافت نمیکند. کد زیر نحوه پیاده سازی متد transform را نمایش میدهد.
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'perNumber'}) export class PerNumberPipe implements PipeTransform { transform(input: string): string{ if (input == undefined) return ''; var str = input.toString().trim(); if (str === "") return ""; str = str.replace(/0/g, '۰'); str = str.replace(/1/g, '۱'); str = str.replace(/2/g, '۲'); str = str.replace(/3/g, '۳'); str = str.replace(/4/g, '۴'); str = str.replace(/5/g, '۵'); str = str.replace(/6/g, '۶'); str = str.replace(/7/g, '۷'); str = str.replace(/8/g, '۸'); str = str.replace(/9/g, '۹'); return str; } }
نحوه معرفی Pipe سفارشی به برنامه
حالا جهت استفاده از Pipe سفارشی در کامپوننتهای خود کافی است آنرا یکبار در قسمت declarations در AppModule خود تعریف کنید.
import { PerNumberPipe } from './pipes/per-number.pipe.ts' ... declarations: [PerNumberPipe]
نحوه استفاده از Pipeهای سفارشی
نحوه استفاده از Pipeهای سفارشی، دقیقا مشابه Pipeهای از قبل ساخته شده Angular میباشد.
<h3>{{'12345679' | perNumber}}</h3>
یکسری از Pipeهای مربوط به زبان فارسی در گیتهاب بنده پیاده سازی شده است که نیازمند همکاری دوستان است. لطفا جهت همکاری برای ساخت یک ابزار جامع برای زبان فارسی در Angular به این لینک مراجعه کنید.
Pipeها و تشخیص تغییرات
Angular برای اعمال Pipe بر روی Template expressions بایستی تمامی رخدادهای برنامه را تحت نظر قرار داده و با مشاهده هر تغییری بر روی عبارت ورودی Pipe، فراخوانی Pipe را آغاز کند. از جمله این رخدادها میتوان به رخداد mouse move، timer tick، server response و فشرده شدن کلیدهای ماوس و یا کیبورد اشاره کرد. واضح است که بررسی تغییرات عبارت در این همه رخداد میتواند مخرب باشد و بر روی کارآئی (Performance) تاثیر منفی خواهد گذاشت. اما Angular برای حل این مشکل و همچنین هنگام مشاهده سریع تغییرات هنگام استفاده از Pipeها، الگوریتمهای سریع و سادهای در نظر گرفته است.
در قسمت بعد با انواع Pipeها در Angular و همچنین نحوه پیاده سازی آنها، آشنا خواهیم شد.
نکته: آشنایی اولیه با مفاهیم MEF از ملزومات این بخش است.
»ابتدا یک Class Library به نام Views ایجاد نمایید و اینترفیس زیر را به صورت زیر در آن تعریف نمایید. این اینترفیس رابط بین کنترلر و View از طریق ViewModel خواهد بود.
public interface IBookView : IView { void Show(); void Close(); }
Install-Package WAF
<Window x:Class="Shell.BookShell" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Book View" Height="350" Width="525"> <Grid> <DataGrid ItemsSource="{Binding Books}" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="400" Height="200"> <DataGrid.Columns> <DataGridTextColumn Header="Code" Binding="{Binding Code}" Width="100"></DataGridTextColumn> <DataGridTextColumn Header="Title" Binding="{Binding Title}" Width="300"></DataGridTextColumn> </DataGrid.Columns> </DataGrid> </Grid> </Window>
[Export(typeof(IBookView))] [PartCreationPolicy(CreationPolicy.NonShared)] public partial class BookShell : Window, IBookView { public BookShell() { InitializeComponent(); } }
»یک Class Library به نام Models بسازید و بعد از ایجاد آن، کلاس زیر را به عنوان مدل Book در آن کپی کنید:
public class Book { public int Code { get; set; } public string Title { get; set; } }
[Export] [Export(typeof(ViewModel<IBookView>))] public class BookViewModel : ViewModel<IBookView> { [ImportingConstructor] public BookViewModel(IBookView view) : base(view) { } public ObservableCollection<Book> Books { get; set; } }
به دلیل اینکه سازنده پیش فرض در این کلاس وجود ندارد حتما باید از ImportingConstructor استفاده نماییم تا CompositionContainer در هنگام عملیات وهله سازی Exception صادر نکند.
»بخش بعدی ساخت یک Class Library دیگر به نام Controllers است. در این Library نیز بعد از ارجاع به اسمبلیهای زیر کتابخانه WAF را نصب نمایید.
- Views
- Models
- ViewModels
- System.ComponentModel.Composition
[Export] public class BookController { [ImportingConstructor] public BookController(BookViewModel viewModel) { ViewModelCore = viewModel; } public BookViewModel ViewModelCore { get; private set; } public void Run() { var result = new List<Book>(); result.Add(new Book { Code = 1, Title = "Book1" }); result.Add(new Book { Code = 2, Title = "Book2" }); result.Add(new Book { Code = 3, Title = "Book3" }); ViewModelCore.Books = new ObservableCollection<Models.Book>(result); (ViewModelCore.View as IBookView).Show(); } }
طراحی Bootstrapper
در پروژههای ماژولار Bootstrapper از ملزومات جدانشدنی این گونه پروژه هاست. برای این کار ابتدا یک WPF Application دیگر به نام Bootstrapper ایجاد نماید. سپس ارجاعی به اسمبلیهای زیر را در آن قرار دهید:
»Controllers
»Views
»ViewModels
»Shell
»System.ComponentModel.Composition
»نصب بسته WAF با استفاده از nuget
حال یک کلاس به نام AppBootstrapper ایجاد نمایید و کدهای زیر را در آن کپی نمایید:
public class AppBootstrapper { public CompositionContainer Container { get; private set; } public AggregateCatalog Catalog { get; private set; } public void Run() { Catalog = new AggregateCatalog(); Catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly())); Catalog.Catalogs.Add(new AssemblyCatalog(String.Format("{0}\\{1}", Environment.CurrentDirectory, "Shell.dll"))); Catalog.Catalogs.Add(new AssemblyCatalog(String.Format("{0}\\{1}", Environment.CurrentDirectory, "ViewModels.dll"))); Catalog.Catalogs.Add(new AssemblyCatalog(String.Format("{0}\\{1}", Environment.CurrentDirectory, "Controllers.dll"))); Container = new CompositionContainer(Catalog); var batch = new CompositionBatch(); batch.AddExportedValue(Container); Container.Compose(batch); var bookController = Container.GetExportedValue<BookController>(); bookController.Run(); } }
نکته بخش Startup را از فایل App.Xaml خذف نمایید و در متد Startup این فایل کد زیر را کپی کنید:
public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { new Bootstrapper.AppBootstrapper().Run(); } }
نکته: میتوان بخش اسکن اسمبلیها را توسط یک DirecotryCatalog به صورت زیر خلاصه کرد:
Catalog.Catalogs.Add(new DirectoryCatalog(Environment.CurrentDirectory));
نکته: میتوان به جای جداسازی فیزیکی لایهها آنها را از طریق Directoryها به صورت منطقی در قالب یک اسمبلی نیز مدیریت کرد.
نکته: بهتر است به جای رفرنس مستقیم اسمبلیها به Bootstrapper با استفاده از Pre post build در قسمت Build Event، اسمبلیهای مورد نظر را در یک مسیر Build کپی نمایید که روش آن به تفصیل در این پست و این پست شرح داده شده است.
دانلود سورس پروژه
JSON.parse: unexpected character at line 1 column 1 of the JSON data
Thread.CurrentThread.CurrentUICulture = new CultureInfo("fa-IR"); Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("fa-IR");
[Required(ErrorMessageResourceName = "ResourceKeyName", ErrorMessageResourceType = typeof(<SolutionName>.Resources.<ResourceClassName>))]
public class LocalizationDisplayNameAttribute : DisplayNameAttribute { private readonly DisplayAttribute _display; public LocalizationDisplayNameAttribute(string resourceName, Type resourceType) { _display = new DisplayAttribute { ResourceType = resourceType, Name = resourceName }; } public override string DisplayName { get { try { return _display.GetName(); } catch (Exception) { return _display.Name; } } } }
public class LocalizationDisplayNameAttribute : DisplayNameAttribute { private readonly PropertyInfo nameProperty; public LocalizationDisplayNameAttribute(string displayNameKey, Type resourceType = null) : base(displayNameKey) { if (resourceType != null) nameProperty = resourceType.GetProperty(base.DisplayName, BindingFlags.Static | BindingFlags.Public); } public override string DisplayName { get { if (nameProperty == null) base.DisplayName; return (string)nameProperty.GetValue(nameProperty.DeclaringType, null); } } }
[LocalizationDisplayName("ResourceKeyName", typeof(<SolutionName>.Resources.<ResourceClassName>))]
public class BaseController : Controller { private const string LanguageCookieName = "MyLanguageCookieName"; protected override void ExecuteCore() { var cookie = HttpContext.Request.Cookies[LanguageCookieName]; string lang; if (cookie != null) { lang = cookie.Value; } else { lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR"; var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) }; HttpContext.Response.SetCookie(httpCookie); } Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang); base.ExecuteCore(); } }
public class LocalizationActionFilterAttribute : ActionFilterAttribute { private const string LanguageCookieName = "MyLanguageCookieName"; public override void OnActionExecuting(ActionExecutingContext filterContext) { var cookie = filterContext.HttpContext.Request.Cookies[LanguageCookieName]; string lang; if (cookie != null) { lang = cookie.Value; } else { lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR"; var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) }; filterContext.HttpContext.Response.SetCookie(httpCookie); } Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang); base.OnActionExecuting(filterContext); } }
<select id="langs" onchange="languageChanged()"> <option value="fa-IR">فارسی</option> <option value="en-US">انگلیسی</option> </select> <script type="text/javascript"> function languageChanged() { setCookie("MyLanguageCookieName", $('#langs').val(), 365); window.location.reload(); } document.ready = function () { $('#langs').val(getCookie("MyLanguageCookieName")); }; function setCookie(name, value, exdays, path) { var exdate = new Date(); exdate.setDate(exdate.getDate() + exdays); var newValue = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString()) + ((path == null) ? "" : "; path=" + path) ; document.cookie = name + "=" + newValue; } function getCookie(name) { var i, x, y, cookies = document.cookie.split(";"); for (i = 0; i < cookies.length; i++) { x = cookies[i].substr(0, cookies[i].indexOf("=")); y = cookies[i].substr(cookies[i].indexOf("=") + 1); x = x.replace(/^\s+|\s+$/g, ""); if (x == name) { return unescape(y); } } } </script>
GET https://www.dntips.ir HTTP/1.1 ... Accept-Language: fa-IR,en-US;q=0.5 ...
Accept-Language: fa-IR,fa;q=0.8,en-US;q=0.5,ar-BH;q=0.3
<system.web> <globalization enableClientBasedCulture="true" uiCulture="auto" culture="auto"></globalization> </system.web>
var langs = filterContext.HttpContext.Request.UserLanguages;
routes.MapRoute( "Localization", // Route name "{lang}/{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults );
public class BaseController : Controller { protected override void ExecuteCore() { var lang = RouteData.Values["lang"]; if (lang != null && !string.IsNullOrWhiteSpace(lang.ToString())) { Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang.ToString()); } base.ExecuteCore(); } }
public class BaseController : Controller { protected override void OnActionExecuted(ActionExecutedContext context) { var view = context.Result as ViewResultBase; if (view == null) return; // not a view var viewName = view.ViewName; view.ViewName = GetGlobalizationViewName(viewName, context); base.OnActionExecuted(context); } private static string GetGlobalizationViewName(string viewName, ControllerContext context) { var cultureName = Thread.CurrentThread.CurrentUICulture.Name; if (cultureName == "en-US") return viewName; // default culture if (string.IsNullOrEmpty(viewName)) return context.RouteData.Values["action"] + "." + cultureName; // "Index.fa" int i; if ((i = viewName.IndexOf('.')) > 0) // ex: Index.cshtml return viewName.Substring(0, i + 1) + cultureName + viewName.Substring(i); // "Index.fa.cshtml" return viewName + "." + cultureName; // "Index" ==> "Index.fa" } }
public sealed class RazorGlobalizationViewEngine : RazorViewEngine { protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) { return base.CreatePartialView(controllerContext, GetGlobalizationViewPath(controllerContext, partialPath)); } protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) { return base.CreateView(controllerContext, GetGlobalizationViewPath(controllerContext, viewPath), masterPath); } private static string GetGlobalizationViewPath(ControllerContext controllerContext, string viewPath) { //var controllerName = controllerContext.RouteData.GetRequiredString("controller"); var request = controllerContext.HttpContext.Request; var lang = request.Cookies["MyLanguageCookie"]; if (lang != null && !string.IsNullOrEmpty(lang.Value) && lang.Value != "en-US") { var localizedViewPath = Regex.Replace(viewPath, "^~/Views/", string.Format("~/Views/Globalization/{0}/", lang.Value)); if (File.Exists(request.MapPath(localizedViewPath))) viewPath = localizedViewPath; } return viewPath; }
protected void Application_Start() { ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(new RazorGlobalizationViewEngine()); }
<?xml version="1.0" encoding="utf-8"?> <root> <!-- Microsoft ResX Schema ... --> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> ... </xsd:schema> <resheader name="resmimetype"> <value>text/microsoft-resx</value> </resheader> <resheader name="version"> <value>2.0</value> </resheader> <resheader name="reader"> <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="RightToLeft" xml:space="preserve"> <value>false</value> <comment>RightToleft is false in English!</comment> </data> </root>
مجوز استفاده از فایلهای جاوا اسکریپتی آن MIT است؛ به این معنا که در هر نوع پروژهای قابل استفاده است. مجوز استفاده از کامپوننتهای سمت سرور آن که برای نمونه جهت ASP.NET MVC یک سری HTML Helper را تدارک دیدهاند، تجاری میباشد. در ادامه قصد داریم صرفا از فایلهای JS عمومی آن استفاده کنیم.
دریافت jqGrid
برای دریافت jqGrid میتوانید به مخزن کد آن، در آدرس https://github.com/tonytomov/jqGrid/releases و یا از طریق NuGet اقدام کنید:
PM> Install-Package Trirand.jqGrid
از jQuery UI برای تولید صفحات جستجوی بر روی رکوردها و همچنین تولید خودکار صفحات ویرایش و یا افزودن رکوردها استفاده میکند. به علاوه آیکنها، قالب و رنگ خود را نیز از jQuery UI دریافت میکند. بنابراین اگر قصد تغییر قالب آنرا داشتید تنها کافی است یک قالب استاندارد دیگر jQuery UI را مورد استفاده قرار دهید.
تنظیمات اولیه فایل Layout سایت
پس از دریافت بستهی نیوگت jqGrid، نیاز است فایلهای مورد نیاز اصلی آنرا به شکل زیر به فایل layout پروژه اضافه کرد:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/Content/themes/base/jquery.ui.all.css" rel="stylesheet" /> <link href="~/Content/jquery.jqGrid/ui.jqgrid.css" rel="stylesheet" /> <link href="~/Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div> @RenderBody() </div> <script src="~/Scripts/jquery-1.7.2.min.js"></script> <script src="~/Scripts/jquery-ui-1.8.11.min.js"></script> <script src="~/Scripts/i18n/grid.locale-fa.js"></script> <script src="~/Scripts/jquery.jqGrid.min.js"></script> @RenderSection("Scripts", required: false) </body> </html>
این گرید به همراه فایل زبان فارسی grid.locale-fa.js نیز میباشد که در کدهای فوق پیوست شدهاست. البته اگر فرصت کردید نیاز است کمی ترجمههای آن بهبود پیدا کنند.
تنظیمات ثانویه site.css
.ui-widget { } /*how to move jQuery dialog close (X) button from right to left*/ .ui-jqgrid .ui-jqgrid-caption-rtl { text-align: center !important; } .ui-dialog .ui-dialog-titlebar-close { left: .3em !important; } .ui-dialog .ui-dialog-title { margin: .1em 0 .1em .8em !important; direction: rtl !important; float: right !important; }
همچنین محل قرار گیری دکمهی بسته شدن دیالوگها و راست به چپ کردن عناوین آنها نیز در اینجا قید شدهاند.
مدل برنامه
در ادامه قصد داریم لیستی از محصولات را با ساختار ذیل، توسط jqGrid نمایش دهیم:
namespace jqGrid01.Models { public class Product { public int Id { set; get; } public string Name { set; get; } public decimal Price { set; get; } public bool IsAvailable { set; get; } } }
ساختار دادهای مورد نیاز توسط jqGrid
jqGrid مستقل است از فناوری سمت سرور. بنابراین هر چند در عنوان بحث ASP.NET MVC ذکر شدهاست، اما از ASP.NET MVC صرفا جهت بازگرداندن خروجی JSON استفاده خواهیم کرد و این مورد در هر فناوری سمت سرور دیگری نیز میتواند انجام شود.
using System.Collections.Generic; namespace jqGrid01.Models { public class JqGridData { public int Total { get; set; } public int Page { get; set; } public int Records { get; set; } public IList<JqGridRowData> Rows { get; set; } public object UserData { get; set; } } public class JqGridRowData { public int Id { set; get; } public IList<string> RowCells { set; get; } } }
Total، نمایانگر تعداد صفحات اطلاعات است. عدد Page، شماره صفحهی جاری است. عدد Records، تعداد کل رکوردهای گزارش را مشخص میکند. ساختار ردیفهای آن نیز تشکیل شدهاست از یک Id به همراه سلولهایی که باید با فرمت string، بازگشت داده شوند.
UserData اختیاری است. برای مثال اگر خواستید جمع کل صفحه را در ذیل گرید نمایش دهید، میتوانید یک anonymous object را در اینجا مقدار دهی کنید. خاصیتهای آن دقیقا باید با نام خاصیتهای ستونهای متناظر، یکی باشند. برای مثال اگر میخواهید عددی را در ستون Id، در فوتر گرید نمایش دهید، باید نام خاصیت را Id ذکر کنید.
کدهای سمت کلاینت گرید
در اینجا کدهای کامل سمت کلاینت گرید را ملاحظه میکنید:
@{ ViewBag.Title = "Index"; } <div dir="rtl" align="center"> <div id="rsperror"></div> <table id="list" cellpadding="0" cellspacing="0"></table> <div id="pager" style="text-align:center;"></div> </div> @section Scripts { <script type="text/javascript"> $(document).ready(function () { $('#list').jqGrid({ caption: "آزمایش اول", //url from wich data should be requested url: '@Url.Action("GetProducts","Home")', //type of data datatype: 'json', jsonReader: { root: "Rows", page: "Page", total: "Total", records: "Records", repeatitems: true, userdata: "UserData", id: "Id", cell: "RowCells" }, //url access method type mtype: 'GET', //columns names colNames: ['شماره', 'نام محصول', 'موجود است', 'قیمت'], //columns model colModel: [ { name: 'Id', index: 'Id', align: 'right', width: 50, sorttype: "number" }, { name: 'Name', index: 'Name', align: 'right', width: 300 }, { name: 'IsAvailable', index: 'IsAvailable', align: 'center', width: 100, formatter: 'checkbox' }, { name: 'Price', index: 'Price', align: 'center', width: 100, sorttype: "number" } ], //pager for grid pager: $('#pager'), //number of rows per page rowNum: 10, rowList: [10, 20, 50, 100], //initial sorting column sortname: 'Id', //initial sorting direction sortorder: 'asc', //we want to display total records count viewrecords: true, altRows: true, shrinkToFit: true, width: 'auto', height: 'auto', hidegrid: false, direction: "rtl", gridview: true, rownumbers: true, footerrow: true, userDataOnFooter: true, loadComplete: function() { //change alternate rows color $("tr.jqgrow:odd").css("background", "#E0E0E0"); }, loadError: function(xhr, st, err) { jQuery("#rsperror").html("Type: " + st + "; Response: " + xhr.status + " " + xhr.statusText); } //, loadonce: true }) .jqGrid('navGrid', "#pager", { edit: false, add: false, del: false, search: false, refresh: true }) .jqGrid('navButtonAdd', '#pager', { caption: "تنظیم نمایش ستونها", title: "Reorder Columns", onClickButton: function() { jQuery("#list").jqGrid('columnChooser'); } }); }); </script> }
Div سومی با id مساوی rsperror نیز تعریف شدهاست که از آن جهت نمایش خطاهای بازگشت داده شده از سرور استفاده کردهایم.
- در ادامه نحوهی فراخوانی افزونهی jqGrid را بر روی جدول list ملاحظه میکنید.
- خاصیت caption، عنوان نمایش داده شده در بالای گرید را مقدار دهی میکند:
- خاصیت url، به آدرسی اشاره میکند که قرار است ساختار JqGridData ایی را که پیشتر در مورد آن بحث کردیم، با فرمت JSON بازگشت دهد. در اینجا برای مثال به یک اکشن متد کنترلری در یک پروژهی ASP.NET MVC اشاره میکند.
- datatype را برابر json قرار دادهایم. از نوع xml نیز پشتیبانی میکند.
- شیء jsonReader را از این جهت مقدار دهی کردهایم تا بتوانیم شیء JqGridData را با اصول نامگذاری دات نت، هماهنگ کنیم. برای درک بهتر این موضوع، فایل jquery.jqGrid.src.js را باز کنید و در آن به دنبال تعریف jsonReader بگردید. به یک چنین مقادیر پیش فرضی خواهید رسید:
ts.p.jsonReader = $.extend(true,{ root: "rows", page: "page", total: "total", records: "records", repeatitems: true, cell: "cell", id: "id", userdata: "userdata", subgrid: {root:"rows", repeatitems: true, cell:"cell"} },ts.p.jsonReader);
- در ادامه mtype به GET تنظیم شدهاست. در اینجا مشخص میکنیم که عملیات Ajax ایی دریافت اطلاعات از سرور توسط GET انجام شود یا برای مثال توسط POST.
- خاصیت colNames، معرف نام ستونهای گرید است. برای اینکه این نامها از راست به چپ نمایش داده شوند، باید خاصیت direction به rtl تنظیم شود.
- colModel آرایهای است که تعاریف ستونها را در بر دارد. مقدار name آن باید یک نام منحصربفرد باشد. از این نام در حین جستجو یا ویرایش اطلاعات استفاده میشود. مقدار index نامی است که جهت مرتب سازی اطلاعات، به سرور ارسال میشود. تنظیم sorttype در اینجا مشخص میکند که آیا به صورت پیش فرض، ستون جاری رشتهای مرتب شود یا اینکه برای مثال عددی پردازش گردد. مقادیر مجاز آن text (مقدار پیش فرض)، float، number، currency، numeric، int ، integer، date و datetime هستند.
- در ستون IsAvailable، مقدار formatter نیز تنظیم شدهاست. در اینجا توسط formatter، نوع bool دریافتی با یک checkbox نمایش داده خواهد شد.
- خاصیت pager به id متناظری در صفحه اشاره میکند.
- توسط rowNum مشخص میکنیم که در هر صفحه چه تعداد رکورد باید نمایش داده شوند.
- تعداد رکوردهای نمایش داده شده را میتوان توسط rowList پویا کرد. در اینجا آرایهای را ملاحظه میکنید که توسط اعداد آن، کاربر امکان انتخاب صفحاتی مثلا 100 ردیفه را نیز پیدا میکند. rowList به صورت یک dropdown در کنار عناصر راهبری صفحه در فوتر گرید ظاهر میشود.
- خاصیت sortname، نحوهی مرتب سازی اولیه گرید را مشخص میکند.
- خاصیت sortorder، جهت مرتب سازی اولیهی گردید را تنظیم میکند.
- viewrecords: تعداد رکوردها را در نوار ابزار پایین گرید نمایش میدهد.
- altRows: سبب میشود رنگ متن ردیفها یک در میان متفاوت باشد.
- shrinkToFit: به معنای تنظیم خودکار اندازهی سلولها بر اساس اندازهی دادهای است که دریافت میکنند.
- width: عرض گرید، که در اینجا به auto تنظیم شدهاست.
- height: طول گرید، که در اینجا به auto جهت محاسبهی خودکار، تنظیم شدهاست.
- gridview: برای بالا بردن سرعت نمایشی به true تنظیم شدهاست. در این حالت کل ردیف یکباره درج میشود. اگر از subgird یا حالت نمایش درختی استفاده شود، باید این خاصیت را false کرد.
- rownumbers: ستون سمت راست شماره ردیفهای خودکار را نمایش میدهد.
- footerrow: سبب نمایش ردیف فوتر میشود.
- userDataOnFooter: سبب خواهد شد تا خاصیت UserData مقدار دهی شده، در ردیف فوتر ظاهر شود.
- loadComplete : یک callback است که زمان پایان بارگذاری صفحهی جاری را مشخص میکند. در اینجا با استفاده از jQuery سبب شدهایم تا رنگ پس زمینهی ردیفها یک در میان تغییر کند.
- loadError: اگر از سمت سرور خطایی صادر شود، در این callback قابل دریافت خواهد بود.
- در ادامه توسط فراخوانی متد jqGrid با پارامتر navGrid، در ناحیه pager سبب نمایش دکمه refresh شدهایم. این دکمه سبب بارگذاری مجدد اطلاعات گردید از سرور میشود.
- همچنین به کمک متد jqGrid با پارامتر navButtonAdd در ناحیه pager، سبب نمایش دکمهای که صفحهی انتخاب ستونها را ظاهر میکند، خواهیم شد.
پیشنیاز کدهای سمت سرور jqGrid
اگر به تنظیمات گرید دقت کرده باشید، خاصیت index ستونها، نامی است که به سرور، جهت اطلاع رسانی در مورد فیلتر اطلاعات و مرتب سازی مجدد آنها ارسال میگردد. این نام، بر اساس کلیک کاربر بر روی ستونهای موجود، هر بار میتوان متفاوت باشد. بنابراین بجای if و else نوشتنهای طولانی جهت مرتب سازی اطلاعات، میتوان از کتابخانهی معروفی به نام dynamic LINQ استفاده کرد.
PM> Install-Package DynamicQuery
کدهای سمت سرور بازگشت اطلاعات به فرمت JSON
در کدهای سمت کلاینت، به اکشن متد GetProducts اشاره شده بود. تعاریف کامل آنرا در ذیل مشاهده میکنید:
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Web.Mvc; using jqGrid01.Models; using jqGrid01.Extensions; // for dynamic OrderBy namespace jqGrid01.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } public ActionResult GetProducts(string sidx, string sord, int page, int rows) { var list = ProductDataSource.LatestProducts; var pageIndex = page - 1; var pageSize = rows; var totalRecords = list.Count; var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize); var products = list.AsQueryable() .OrderBy(sidx + " " + sord) .Skip(pageIndex * pageSize) .Take(pageSize) .ToList(); var jqGridData = new JqGridData { UserData = new // نمایش در فوتر { Name = "جمع صفحه", Price = products.Sum(x => x.Price) }, Total = totalPages, Page = page, Records = totalRecords, Rows = (products.Select(product => new JqGridRowData { Id = product.Id, RowCells = new List<string> { product.Id.ToString(CultureInfo.InvariantCulture), product.Name, product.IsAvailable.ToString(), product.Price.ToString(CultureInfo.InvariantCulture) } })).ToList() }; return Json(jqGridData, JsonRequestBehavior.AllowGet); } } }
- امضای متد GetProducts نیز مهم است. دقیقا همین پارامترها با همین نامها از طرف jqGrid به سرور ارسال میشوند که توسط آنها ستون مرتب سازی، جهت مرتب سازی، صفحهی جاری و تعداد ردیفی که باید بازگشت داده شوند، قابل دریافت است.
- در این کدها دو قسمت مهم وجود دارند:
الف) متد OrderBy نوشته شده، به صورت پویا عمل میکند و از کتابخانهی Dynamic LINQ مایکروسافت بهره میبرد.
به علاوه توسط Take و Skip کار صفحه بندی و بازگشت تنها بازهای از اطلاعات مورد نیاز، انجام میشود.
ب) لیست جنریک محصولات، در نهایت باید با فرمت JqGridData به صورت JSON بازگشت داده شود. نحوهی این Projection را در اینجا میتوانید ملاحظه کنید.
هر ردیف این لیست، باید تبدیل شود به ردیفی از جنس JqGridRowData، تا توسط jqGrid قابل پردازش گردد.
- توسط مقدار دهی UserData، برچسبی را در ذیل ستون Name و مقداری را در ذیل ستون Price نمایش خواهیم داد.
برای مطالعهی بیشتر
بهترین راهنمای جزئیات این Grid، مستندات آنلاین آن هستند: http://www.trirand.com/jqgridwiki/doku.php?id=wiki:jqgriddocs
همچنین این مستندات را با فرمت PDF نیز میتوانید مطالعه کنید: http://www.trirand.com/blog/jqgrid/downloads/jqgriddocs.pdf
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid01.zip
مثالهای سری jqGrid تغییرات زیادی داشتند. برای دریافت آنها به این مخزن کد مراجعه کنید.