שמירת צילומי מסך של אתרי אינטרנט דרך PHP

בפוסט הזה כתבתי על נושא קצת יותר מעניין: התממשקות בין מספר תוכנות. נלמד איך לשמור צילומי מסך של אתרי אינטרנט באמצעות PHP, תוכנה קטנה ב-C, ודפדפן.
נדרש ידע מוקדם ב-C, ואם אפשר גם ב-PHP - מה טוב :-)

כל הקודים במדריך נכתבו ונבדקו תחת Windows. הם לא יעבדו במערכות הפעלה אחרות, אז חבל על הנסיונות. כן, אני יודע שזה מבאס, אבל תתמודדו D-:

למה זה טוב?

  • אם אתם רוצים לבנות אינדקס אתרים שמציג תמונות של האתרים (בדומה לאינדקס של תפוז)
  • אם אתם מארחים מספר אתרים ואתם רוצים ליצור עמוד עם צילומי מסך עדכניים של כל אתר.
  • אם אתם רוצים לבדוק במהירות מתי אתר מסויים מתעדכן בלי להרשם ל-RSS של האתר (או, במקרה הגרוע, אם אין בו בכלל RSS).
  • אם אתם סתם רוצים לראות את התוצאה :-)
בחייכם, תהיו יצירתיים קצת...

עיקרון הפעולה

אז ככה. ב-PHP 5.2.2 נוספו שתי פונקציות מעניינות להרחבת ה-GD האהובה: imagegrabwindow ו-imagegrabscreen, שמאפשרות "לצלם" את תוכנו של חלון מסויים או את תוכנו של המסך כולו, בהתאמה. חלקכם וודאי כבר מבינים את הרעיון - פשוט נפתח חלון דפדפן חדש דרך תוכנה שנכתוב ב-C, ננווט לאתר הרצוי, נצלם את תוכנו של החלון עם imagegrabwindow, ונשמור את התמונה בקובץ חדש בעזרת הפונקציה imagejpeg/imagepng. אתם צודקים כמובן, אבל זה לא פשוט כמו שאתם חושבים :-)

למה דווקא בשיטה הזאת?

זו לא השיטה האידיאלית ובוודאי שלא האופטימלית... אז למה אני משתמש בה בכל זאת? פשוט מאוד - אין לי כוח לכתוב הרחבה ל-PHP או ליצור אובייקט COM :-)

דפדפני וחיות אחרות

בתיעוד הרשמי של הפונקציה imagegrabwindow קיימת דוגמה שעושה בדיוק את מה שאנחנו רוצים. אז זהו, שלא בדיוק. הדוגמה שם פותחת (באמצעות אובייקט COM אם זה ממש מעניין אתכם) חלון חדש של הדפדפן Internet Explorer, מנווטת לאתר המבוקש, מחכה עד שהוא יעלה, מצלמת אותו, וסוגרת אותו. זה לא טוב לנו ממספר סיבות, שאלה עיקרן:
  1. מי אמר שאנחנו רוצים להשתמש ב-Internet Explorer?
  2. מה אם אנחנו לא רוצים שגם התפריטים של החלון יהיו בתמונה?
  3. מי אמר שאנחנו רוצים להשתמש ב-Internet Explorer?!
הבעיה מתחילה כאן. אנחנו לא יכולים פשוט להמיר את הדוגמה להתממשקות מול דפדפן אחר, מהסיבה הפשוטה שלא לכל דפדפן יש אובייקט COM זמין. גם אם היה, עדיין נשארת לנו הבעיה של צילום התוכן בלבד, ללא התפריטים :) נכון, אפשר למדוד את הרוחב והגובה של איזור התוכן ולחתוך רק אותו, אבל אם נחליט לעבור דפדפן או לשנות את גודל החלון זה יהיה די מעצבן לחשב את זה מחדש...

המצרכים

קודם כל - תוכנה שבעזרת WinAPI יוצרת חלון חדש של Opera, הדפדפן האהוב עלי, מנווטת לאתר שאותו נרצה לצלם, ומדפיסה לפלט הסטנדרטי (stdout) את הידית לחלון (מספר מזהה ייחודי שמוקצה על ידי Windows). קוד ה-PHP שלנו יתממשק לתוכנה באמצעות הפונקציה system, שמאפשרת להריץ פקודת CMD ולקבל את הפלט הסטנדרטי (בעיקרון כל דבר שאנחנו מדפיסים עם printf) שלה, שיהיה כמובן הידית לחלון הדפדפן. זה ה-"שלד" הבסיסי של קוד ה-PHP שלנו (שימו לב שיש עוד כמה שלבים בדרך שלא נמצאים כאן): <?php $browser_window_handle = system('OperaSnapshot.exe {הכתובת שאנחנו רוצים לצלם}'); imagejpeg(imagegrabwindow($browser_window_handle)); ?>

קדימה לעבודה

כדי לצלם את תוכן חלון הדפדפן שלנו, נצטרך למצוא את הידית לחלון, ולהעביר אותה לפונקציה הנ"ל . הידית היא מספר מזהה ייחודי שניתן לכל חלון ב-Windows. ב-C יש למזהה הזה סוג מוגדר בשם HWND (Handle to a Window). יש מספר דרכים למצוא את הידית לחלון. הנה הדרך הראשונה שבה נקטתי, והסבר קצר מדוע היא לא טובה:
לחלונות ב-Windows יש מאפיין נוסף שנקרא Class Name, ואתם יכולים להתייחס אליו כאל "סוג החלון". ה-Class Name הוא מחרוזת שכותב התוכנה יכול לקבוע בעצמו - אם, למשל, כתבתי תוכנה בשם BunnyLink, הגיוני שאני אקבע את שם המחלקה של החלון שלי ל-"BunnyLinkClass". את הידית לחלון נוכל למצוא בעזרת הפונקציה FindWindow. הפונקציה מקבלת את שם המחלקה של החלון והכותרת שלו, ומחזירה את הידית של החלון הראשון שמתאים למאפיינים האלה. את הכותרת של החלון שלנו לא תהיה בעיה למצוא - לכל דפדפן יש את מבנה הכותרות שלו, ובמקרה שלנו המבנה יהיה "<Webpage Title> - Opera". אבל רגע, מה עם שם המחלקה?! Opera הוא לא דפדפן קוד פתוח, ולכן אי אפשר להוריד את קוד המקור ולחפש את שם המחלקה שהמתכנתים שלו בחרו. מה נעשה? כבר כתבתי קוד קצר שמוצא את המחלקה (שהיא, דרך אגב, OpWindow) בעזרת הפונקציה EnumWindows, וכאמור תכננתי למצוא את הידית לחלון עם FindWindow. בדיעבד מסתבר שזה די טיפשי - מה אם יש מספר חלונות Opera עם כותרת זהה?
אחרי שיטוט קצר בגוגל מצאתי את הפתרון. בהמשך אתם תראו שכשאנחנו יוצרים את החלון של אופרה, אנחנו מקבלים גם את ה-Thread ID של התהליך. אתם יכולים פשוט להתייחס אליו כאל מספר מזהה של התוכנה (מישהו אמר מידע?). אחר כך נשתמש (כן, שוב) בפונקציה EnumWindows שמונה את כל החלונות הפתוחים ונשווה בין ה-Thread ID של החלון לבין ה-Thread ID של החלונות ש-EnumWindows תמצא. אם לא הבנתם, לא נורא - תבינו בהמשך.

בואו נראה לו מה זה תהליכים

כדי לפתוח חלון חדש של Opera נצטרך להריץ את הקובץ Opera.exe שבדרך כלל ימצא בתיקיה C:\Program Files\Opera. את זה נעשה עם הפונקציה המפחידה במקצת CreateProcess. בלי להכביר מילים, הדרו והריצו את הקוד הבא: #include <windows.h> HWND Opera; BOOL EnumWindowsProc(HWND WindowHandle, LPARAM OperaThreadID) { // נמצא את ה-Thread ID של החלון הנוכחי לצורך השוואה DWORD ThreadID = GetWindowThreadProcessId(WindowHandle, NULL); // אם ה-Thread ID של החלון הנוכחי שווה ל-Thread ID של Opera, סימן שהחלון הנוכחי הוא החלון של Opera if (ThreadID == (DWORD) OperaThreadID) { Opera = WindowHandle; // נציב את החלון במשתנה הגלובלי (מצטער, הייתי חייב) כדי שנוכל להתייחס אליו מאוחר יותר return FALSE; // נגרום ל-EnumWindows לעצור } return TRUE; // אם החלון של אופרה עדיין לא נמצא, EnumWindows צריכה להמשיך לעבור על החלונות } int main(int argc, char * argv[]) { STARTUPINFO StartupInfo; // מאפיינים כלליים של התהליך PROCESS_INFORMATION ProcInfo; // חשוב - הוא יתמלא במידע חיוני כשניצור את התהליך (ה-Thread ID של התהליך למשל) ZeroMemory(&StartupInfo, sizeof(StartupInfo)); // נמלא את המבנה בתווי NULL StartupInfo.cb = sizeof(StartupInfo); // MSDN הכריחו אותי :( ZeroMemory(&ProcInfo, sizeof(ProcInfo)); // אותו דבר כמו קודם... BOOL ProcStatus = CreateProcess("C:\\Program Files\\Opera\\Opera.exe", " http://hsnip.blogspot.com/", NULL, NULL, FALSE, 0, NULL, NULL, &StartupInfo, &ProcInfo); /* מותר גם ככה: BOOL ProcStatus = CreateProcess(NULL, "\"C:\\Program Files\\Opera\\Opera.exe\" http://hsnip.blogspot.com/", NULL, NULL, FALSE, 0, NULL, NULL, &StartupInfo, &ProcInfo); */ if (ProcStatus == TRUE) // if the process was created succesfully { WaitForInputIdle(ProcInfo.hProcess, INFINITE); // נחכה עד שהחלון יווצר Sleep(3000); // שימו לב להסבר בהמשך! EnumWindows(EnumWindowsProc, ProcInfo.dwThreadId); // פה קורה כל הקסם char * WindowTitleBuffer[256]; ZeroMemory(WindowTitleBuffer, 256); // מניעת buffer overflows היא דבר בריא GetWindowText(Opera, WindowTitleBuffer, 255); printf("Opera's Title: %s\n", WindowTitleBuffer); } else printf("Couldn't create process\n"); getchar(); return 0; } אוקיי, עכשיו החלק המשעמם - הערות והסברים.

נתחיל ב-main:

שתי השורות הראשונות הן הצהרות על משתנים כללים. המשתנה הראשון, StartupInfo, יכול להכיל מאפיינים כלליים (גובה החלון, רוחב החלון, וכו') שחלים על התהליך. ב-ProcInfo אנחנו לא נוגעים; הפונקציה CreateProcess תמלא אותו במידע כשנקרא לה. הפונקציה CreateProcess עלולה להרתיע במבט ראשון, אבל זה די פשוט בעצם. הפרמטרים היחידים שמעניינים אותנו כרגע הם שני הראשונים - lpApplicationName ו-lpCommandLine. הראשון מקבל את הנתיב לקובץ שיש להריץ, והשני מקבל את הפרמטרים שניתנים לקובץ. כמו שראיתם בהערה בקוד, אפשר גם לוותר על lpApplicationName ולספק את נתיב הקובץ ב-lpCommandLine, אבל אני בכל זאת מעדיף את זה ככה. במקרה הזה אנחנו מעבירים ל-Opera את כתובת האתר שנרצה לפתוח. אחר כך יש תנאי פשוט שבודק אם התהליך נוצר בהצלחה, ואם כן - אנחנו משתמשים ב-WaitForInputIdle. שימו לב ל-Sleep. הוא לא טוב. פתרון אידיאלי יהיה למצוא את הפקד של טעינת העמוד (אתם יודעים, הדבר הזה שכותב כמה אחוזים מהדף נטענו) ולחכות שהוא יעלם. איך עושים את זה? שאלה מצויינת - אין לי מושג. הבעיה העיקרית היא שהמשתמש בדפדפן יכול לקבוע את המיקום של הפקד, או פשוט להעלים אותו לגמרי. ה-Sleep הוא פתרון גרוע ביותר, משום שייתכן ונתקל באתר איטי שפשוט לא יספיק להטען בשלוש שניות. אם למישהו מכם יש פתרון יותר טוב, אני אהיה יותר משמח לראות אותו :-)

30 שניות על EnumWindowsProc:

הפונקציה EnumWindows מקבלת בפרמטר השני משתנה שאותו היא מעבירה לפונקציה EnumWindowsProc. את המשתנה הזה אנחנו מגדירים - הוא נוצר במיוחד בשבילנו. נעביר לו את ה-Thread ID של התהליך שיצרנו ב-main, כי אחרת איך נוכל לבצע את ההשוואה? אני מקווה שהחלק הזה ברור. אם לא, אתם מוזמנים להגיב ולשאול.

העם דורש HWND!

בסדר בסדר, תרגעו. ניתן להדפיס את הידית לחלון של Opera בעזרת שינוי קל בקוד הנ"ל. שימו לב לשורות האלו: char * WindowTitleBuffer[256]; ZeroMemory(WindowTitleBuffer, 256); GetWindowText(Opera, WindowTitleBuffer, 255); printf("Opera's Title: %s\n", WindowTitleBuffer); בשלב הזה כבר נתון לנו המאפיין הכי חשוב של החלון שיצרנו - הידית שלו, שמוחזקת במשתנה Opera. חשוב לציין שהידית היא מספר הקסדצימלי (בסיס 16), ולכן בשביל להדפיס אותה נצטרך להשתמש בדגל %x של הפונקציה printf. הנה השינוי: printf("%x", Opera);

איפה ה-PHP נכנס כאן?

הוא לא, אבל עכשיו הוא יכנס :-)
חייבים לבצע מספר שינויים בקוד כדי שהוא יתאים ל-PHP.
  1. הראשון והחשוב מכולם, זה שהכי קל לפספס, הוא ה-getchar() שלמטה. הוא טריק זול של מפתחי חלונות שנועד להשארת החלון פתוח לאחר סיום ריצת הקוד, ואם נשאיר אותו ונקרא לתוכנה דרך PHP (בעזרת system), התוכנה פשוט לא תסתיים וקוד ה-PHP יחכה... ויחכה... עד שהוא כנראה יגיע ל-max_execution_time, מאפיין מרושע של PHP שקובע את מספר השניות המירבי שבו קוד PHP יכול לרוץ. ככה שקודם כל, לפני כל שינוי אחר, נעיף את ה-getchar()!
  2. שינוי לא פחות חשוב הוא לשנות את הקריאה ל-CreateProcess כך שהכתובת של האתר תלקח מהפרמטר הראשון שמגיע לתוכנה (argv). הסיבה פשוטה - אנחנו לא רוצים מליון צילומי מסך של הבלוג המשעמם שלי, נכון? אחרי השינוי הזה, נוכל לקרוא לתוכנה בצורה הזאת: $return_status = -1; // הסבר בהמשך $browser_window_handle = system('OperaSnapshot.exe http://www.google.com/', $return_status);
  3. כמו כן מומלץ להחזיר 0 בכדי לסמן שריצת התוכנה הצליחה, או -1 אם התוכנה נכשלה. זה די פשוט - אחרי ההדפסה של הידית לחלון של אופרה צריך לכתוב return 0, ובמבנה ה-else שבו אנחנו מדפיסים Couldn't create process יש למחוק את פונקציית ההדפסה ולכתוב במקומה return -1. המספר הזה יכנס למשתנה $return_status (או לכל משתנה אחר שנעביר בפרמטר השני של הפונקציה system) ובאמצעותו נוכל לבדוק אם התוכנה הצליחה או לא, בלי לבצע כל מיני בדיקות מגוחכות על המספר ההקסדצימלי שהתוכנה מדפיסה.
אבל רגע - התוכנה שלנו מחזירה מספר הקסדצימלי, והפונקציה imagegrabwindow בכלל מקבלת מספר שלם (int)! קודם כל לנשום עמוק. ל-PHP יש אחלה פתרון בדמות הפונקציה intval, שמקבלת מחרוזת להמרה ובסיס מספרי, ומחזירה ערך מספרי לפי הבסיס שצויין בפרמטר השני. הנה הקוד: $browser_window_handle = intval($browser_window_handle, 16); עכשיו יש לנו מספר חמוד ומגניב שאפשר להעביר ל-imagegrabwindow ולקבל את צילום המסך של החלון.

ומה עם התפריטים?

כמו שכתבתי קודם, אחת הבעיות של הדוגמה בתיעוד של imagegrabwindow ב-php.net היא שמקבלים גם את התפריטים ואת שורת הכותרת של החלון בצילום המסך. אחת המטרות של התוכנה שלי היא להתגבר על הבעיה הזאת. לשם כך נצטרך להכיר פונקציה חדשה: EnumChildWindows. הפונקציה דומה במהותה ל-EnumWindows, רק שהיא לא מונה את החלונות הראשיים, אלא את החלונות הצאצאים של החלון שמעבירים לה בפרמטר הראשון. כן, מה ששמעתם - כל "פקד" ב-Windows, כל כפתור וכל תיבת בחירה, נחשב לחלון. בדיקה קצרה גילתה לי ששם המחלקה של חלונות התוכן (קרי: החלונות שבהם רואים את האתר עצמו) של Opera הוא גם OpWindow. מכאן הדרך קצרה לפתרון: נשנה את שם המשתנה Opera ל-OperaContent, סתם בשביל לשמור על הסמנטיות, ובמקום להציב בו את הידית של החלון הראשי שנמצא (בפונקציה EnumWindowsProc), נעביר את הידית לפונקציה EnumChildWindows כדי לעבור על החלונות הצאצאים של החלון הראשי של Opera: BOOL EnumChildWindowsProc(HWND ChildWindowHandle, LPARAM NotUsed) { char * ChildWindowClass[256]; ZeroMemory(ChildWindowClass, 256); GetClassName(ChildWindowHandle, ChildWindowClass, 255); if (strcmp(ChildWindowClass, "OpWindow") == 0) // אם שם המחלקה של החלון הצאצא הנוכחי שווה ל-OpWindow, כנראה שמדובר בחלון התוכן { OperaContent = ChildWindowHandle; return FALSE; } return TRUE; }

שלב 9

אממ... אין שלב כזה. מצטער. הנה הקוד הסופי: #include <windows.h> HWND OperaContent; BOOL EnumChildWindowsProc(HWND ChildWindowHandle, LPARAM lParam) { char * ChildWindowClass[256]; ZeroMemory(ChildWindowClass, 256); GetClassName(ChildWindowHandle, ChildWindowClass, 255); if (strcmp(ChildWindowClass, "OpWindow") == 0) { OperaContent = ChildWindowHandle; return FALSE; } return TRUE; } BOOL EnumWindowsProc(HWND WindowHandle, LPARAM OperaThreadID) { DWORD ThreadID = GetWindowThreadProcessId(WindowHandle, NULL); if (ThreadID == (DWORD) OperaThreadID) { EnumChildWindows(WindowHandle, EnumChildWindowsProc, 0); return FALSE; } return TRUE; } int main(int argc, char * argv[]) { STARTUPINFO StartupInfo; PROCESS_INFORMATION ProcInfo; ZeroMemory(&StartupInfo, sizeof(StartupInfo)); StartupInfo.cb = sizeof(StartupInfo); ZeroMemory(&ProcInfo, sizeof(ProcInfo)); char * OperaStartupCommands[512]; ZeroMemory(OperaStartupCommands, 512); sprintf(OperaStartupCommands, " /KioskMode %s", argv[1]); BOOL ProcStatus = CreateProcess("C:\\Program Files\\Opera\\Opera.exe", OperaStartupCommands, NULL, NULL, FALSE, 0, NULL, NULL, &StartupInfo, &ProcInfo); if (ProcStatus == TRUE) { WaitForInputIdle(ProcInfo.hProcess, INFINITE); Sleep(3000); // אני עדיין מבואס בגלל זה... EnumWindows(EnumWindowsProc, ProcInfo.dwThreadId); printf("%x", OperaContent); return 0; } else return -1; } שימו לב שהפרמטר KioskMode/ שהוספתי בסך הכל גורם ל-Opera להפתח על מסך מלא. ה-PHP: <?php $url_shell = escapeshellarg($_GET['url']); // אבטחה מיותרת היא אף פעם לא מיותרת ob_start(); // שומר את הפלט בזיכרון במקום לפלוט אותו $browser_window_handle = system('OperaSnapshot.exe ' . $url_shell, $return_status); ob_clean(); // מוחק את הפלט ששמור בזיכרון כדי שהידית של החלון לא תודפס מהפונקציה system $browser_window_handle = intval($browser_window_handle, 16); imagepng(imagegrabwindow($browser_window_handle), 'grabbed.png'); if ($return_status == 0) { $url_html = htmlspecialchars($_GET['url']); // כן, אני מודה... אני פארנואיד echo '<a href="grabbed.png">לחצו למעבר לצילום המסך של '. $url_html .'</a>'; } else echo 'שגיאה'; ?>

זהו. טענות, מענות, הערות, שאלות, בקשות - כמו תמיד, כפתור התגובה ממש פה למטה :)

מד התקדמות פשוט מבוסס שורת פקודה

במאמר הזה נלמד איך יוצרים מד התקדמות פשוט. ידע בסיסי ב-C או בכל שפת תכנות בעלת תחביר דומה הוא חובה.

מה זה מד התקדמות?

מד התקדמות הוא יסוד גרפי חשוב ביצירת ממשק משתמש קל להבנה. תוכלו למצוא אחד כזה (כמעט) בכל התקנה של תוכנה כלשהי, בכל הורדה של קובץ מהאינטרנט, ובכל צריבה של תקליטור. מה הוא עושה? פשוט מאוד - מראה את ההתקדמות של התהליך בלי להכביד על המשתמש במספרים וטקסטים מיותרים.

אם עדיין לא הבנתם, הנה דוגמה קטנה:

מד התקדמות
מדי התקדמות ממלאים חלק חשוב בממשקי משתמש, גרפיים או לא - אלה מכם שבקיאים במערכות לינוקס וודאי מכירים את מדי התקדמות בשורת הפקודה שמופיעים כשמתקינים חבילה חדשה. זאת המטרה במדריך הזה – לכתוב מד התקדמות מבוסס שורת פקודה (כלומר מורכב מתווים) עם אפשרויות לשליטה במאפייניו הכלליים, כגון אורך, מראה, מהירות שינוי, וכיוצא באלה.

ממה מורכב מד התקדמות?

השאלה נראית די טריוויאלית, אבל היא בכל זאת חשובה מאוד להמשך המדריך. מד התקדמות מורכב ממסילה, שיכולה להיות קו אופקי, מעגל, או כל צורה אחרת, וצורה לציון ההתקדמות – בדרך כלל סתם ריבוע. המסילה היא דמיונית בלבד – היא אינה אלא רצף של צורות פשוטות יותר. מד ההתקדמות שלנו יורכב, כאמור, מטקסט, ולכן נצטרך למצוא תווים מתאימים לשימוש במסילה ובאות ההתקדמות. כמו כן, נצטרך לבחור אורך למד ההתקדמות שניצור – האורך לא עקרוני, אך עם זאת מובן מאליו שלא נבחר באורך של 10,000 תווים. לצורך המדריך בחרתי באורך של עשרה תווים. דוגמה למד ההתקדמות שניצור במהלך המדריך: [++++++----]

טוב, אז מה לכתוב בקוד?

בשלב הזה אתם כבר וודאי מנחשים (גם אם לא, אל תדאגו – פשוט המשיכו לקרוא) מהו רצף הפעולות שאמורות להתרחש לצורך יצירת מד ההתקדמות שלנו. בהנחה שתו המסילה שבחרנו הוא סימן חיסור ותו ההתקדמות הוא סימן חיבור, תהליך ההדפסה אמור להראות בערך ככה: להדפיס סימן חיסור עשר פעמים ברצף, למחוק את כל עשרת סימני החיסור שהדפסנו, להדפיס סימן חיבור – ואז להדפיס תשעה סימני חיסור, למחוק את כל תשעת הסימנים, להדפיס עוד סימן חיבור, להדפיס שמונה סימני חיסור, וכן הלאה – עד שמגיעים למצב שבו הדפסנו עשרה תווי התקדמות (סימני חיבור). מד ההתקדמות יראה בערך ככה: +--------- ++-------- +++------- וכן הלאה. הנה הקוד שיצר את הדוגמה שלמעלה: int i, k; for (i = 0; i < 10; ++i) { for (k = 10-i; k <= 10; ++k) printf("+"); for (k = i+1; k < 10; ++k) printf("-"); printf("\n"); }

"אלו עשרה מדי התקדמות שונים – אני ביקשתי אחד..."

נכון – זה קורה כי בכל התקדמות אנחנו מדפיסים מד התקדמות חדש ומעודכן. אין שום סיבה (לא אסתטית ולא לוגית) לעשות את זה – המשתמש בסך הכל רוצה לראות את ההתקדמות של התהליך במקום אחד, בלי סיבוכים מיותרים. יש ל-C פתרון מצויין עבור בעיה זו - התו \b (לוכסן ואחריו B קטנה). הוא אולי נראה כמו שני תווים, אבל ב-C הוא מציין תו אחד שמשמעותו BackSpace - כלומר תו מחיקה, בדיוק כמו לחיצה על כפתור BackSpace במקלדת. שמרו את הקוד הבא בקובץ חדש והדרו אותו: #include <stdio.h> int main() { printf("I don’t\b\b\b\b\b\b like bunnies!"); return 0; } ייתכן שחשבתם שהקוד הנ"ל ידפיס את המחרוזת I don’t like bunnies!, אבל אבוי! המילה don’t והרווח שלפניה נמחקו, וכל מה שנשאר זה "I like bunnies!"! כמה צפוי :P עדיין לא הבנתם איך זה עוזר לנו? שימו לב שהוא מוחק תווים בזמן ריצת התוכנה, ולכן אפשר להשתמש בו כדי לדמות תנועה. קחו למשל את הקוד של שלב 1 – כל מה שצריך כדי להוסיף לו "תנועה" זה פשוט למחוק את תווי המסילה אחרי כל הדפסה, להדפיס תו התקדמות נוסף, ומייד אחר כך למלא את המקום הריק בתווי מסילה. בינתיים הקוד יראה בערך כך: int i, k; for (i = 0; i <= 10; ++i) { if (i != 0) printf("+"); for (k = i; k < 10; ++k) printf("-"); for (k = i; k < 10; ++k) printf("\b"); } נכון, זה עדיין לא תנועה – הכל קורה כל כך מהר שאתם פשוט לא מספיקים לראות את השינוי בין שלבי ההתקדמות השונים. כדי להוסיף עיכוב בין הריצות של הלולאה, נצטרך לכתוב פונקציה שתחזור (קרי: תבצע פעולת return) אחרי זמן מסויים שנקבע. כדי לממש את זה, נצטרך לשמור במשתנה את הסכום של מספר אלפיות השניה שעברו מאז הפעלת התוכנה ושל מספר אלפיות השניה שבהן נרצה לעכב את הריצה. לצורך זה נכליל את הספריה התקנית time.h – קיימת בה פונקציה בשם clock שמחזירה טיפוס long המכיל את הזמן (באלפיות שניה) שעבר מאז שהתוכנה הופעלה. אחרי ששמרנו את המידע הדרוש במשתנה, השאר פשוט מאוד – נכתוב לולאה שתרוץ כל עוד הערך המוחזר מ-clock() קטן מהערך השמור במשתנה. void delay(int ms) { long timeout = clock() + ms; while (clock() < timeout) continue; } לפונקציה, מן הסתם, צריך לקרוא אחרי הלולאה שמדפיסה את תווי המסילה. זהו! עכשיו יש לכם מד התקדמות מבוסס שורת פקודה לתפארת מדינת ישראל. עכשיו רק נשאר להכניס אותו לפונקציה מסוגננת ולשחק עם הפרמטרים שלה :-) הנה הקוד המוגמר: #include <stdio.h> #include <time.h> void delay(int ms) { long timeout = clock() + ms; while (clock() < timeout) continue; } void printProgressBar(int length, int interval) { printf("["); int i, k; for (i = 0; i <= length; ++i) { if (i != 0) printf("#"); for (k = i; k < length; ++k) printf("+"); printf("]"); delay(interval); for (k = i; k <= length; ++k) printf("\b"); } } int main() { int i; for (i = 0; i < 50; i += 5) { printProgressBar(i, 100); printf("\n"); } return 0; } אני מקווה שנהניתם לקרוא ולהתנסות במדריך הזה. ביקורות, הצעות שיפור, רעיונות - כולם יתקבלו בברכה :-)