Catone.

Азы C#.

СОДЕРЖАНИЕ
    
Часть 1.
Гла­ва 2. Син­так­сис дек­ла­ра­ций.
Гла­ва 3. Син­так­сис дек­ла­ра­ций мас­си­вов, свойств, де­ле­га­тов. As­sembly.
Гла­ва 4. Ос­нов­ные опе­ра­то­ры и кон­с­т­рук­ты.
Гла­ва 5. Ини­ци­али­за­ция пе­ре­мен­ных. Кол­лек­ции. Ти­пы дан­ных.
Гла­ва 6. При­мер ко­да.
Часть 2.
Гла­ва 2. Ге­не­ало­гия клас­сов.
Гла­ва 3. Ис­к­лю­че­ния.
Гла­ва 4. Муль­ти­по­точ­ность.
Гла­ва 5. Вы­зов фун­к­ций биб­ли­отек Win32 (P/Invo­ke)
Часть 3.
Гла­ва 2. Се­ри­али­за­ция.
Гла­ва 3. Ref­lec­ti­on.
Гла­ва 4. Са­мо­дель­ные эле­мен­ты уп­рав­ле­ния 1.
Гла­ва 5. Са­мо­дель­ные эле­мен­ты уп­рав­ле­ния 2.
Часть 4.
Гла­ва 2. Объ­ект Grap­hics.
Гла­ва 3. Цве­та.
Гла­ва 4. Ка­ран­да­ши.
Гла­ва 5. Кис­ти.
Гла­ва 6. Ге­омет­ри­чес­кие эле­мен­ты.
Гла­ва 7. Мат­рич­ные тран­с­фор­ма­ции.
Гла­ва 8. Гра­фи­чес­кие фай­лы.
Гла­ва 9. При­ме­ры.
При­ло­же­ния.
2. Са­мо­дель­ная го­ло­во­лом­ка.
3. Са­мо­дель­ный из­ме­ня­тель раз­ме­ра кар­ти­нок.
4. Сис­тем­ный ин­ди­ка­тор в за­го­лов­ке ок­на.
5. "Ошку­ри­ва­ние" прог­рам­мы.
6. Пе­рех­ват­чик всех оши­бок.
7. Са­мо­дель­ный бе­ка­пер.
    
    
Часть 1.
    Самые ос­нов­ные ве­щи в язы­ке C# и плат­фор­ме .NET вце­лом.
    
Глава 1. Как читать чужой код.
    
    По прось­бам чи­та­те­лей­, по­пы­тал­ся на­пи­сать об ос­но­вах язы­ка. Впро­чем, толь­ко прис­ту­пив по­нял, что де­ло гиб­лое - что­бы пи­сать об ос­но­вах на­до быть или МУД­рым АКа­де­ми­кОМ, или дей­ст­ви­тель­но иметь та­лант пре­по­да­ва­те­ля. Я та­ко­во­го та­лан­та не имею. По­это­му смо­гу толь­ко крат­ко рас­ска­зать о не­ко­то­рых осо­бен­нос­тях. По­няв, что от та­ко­го тол­ку ма­ло, я ре­шил на­пи­сать для на­ча­ла как чи­тать чу­жой код. Ибо, нас­коль­ко я по­ни­маю, ос­нов­ные проб­ле­мы с по­ни­ма­ни­ем ко­да кро­ют­ся не в нез­на­нии, а в не­уме­нии эти са­мые зна­ния до­быть. В этой об­лас­ти пи­сать про­ще, т.к. мне час­то при­хо­дит­ся чи­тать чу­жой код, са­мо­го раз­но­го уров­ня и на са­мых раз­ных, в том чис­ле не­из­вес­т­ных мне, язы­ках.
    
    Кратко о язы­ке
    Итак язык C# вы­рос из C++, сох­ра­нив ос­нов­ные осо­бен­нос­ти син­так­си­са и опе­ра­то­ры. Да и ос­нов­ные опе­ра­то­ры оди­на­ко­вы во всех язы­ках, так что ес­ли вы зна­ете хоть один язык, то у вас не дол­ж­но воз­ни­кать воп­ро­са что та­кое for, if...then...else, do...whi­le и пр. Не са­мый рас­п­рос­т­ра­нен­ный опе­ра­тор ис­поль­зу­ющий­ся в C# - fo­re­ach. Соб­с­т­вен­но де­ла­ет то, что обоз­на­ча­ет - "for each = для каж­до­го", т.е. про­во­дит цикл для каж­до­го чле­на из мас­си­ва/кол­лек­ции/спис­ка.
    Особенностью всех С-по­доб­ных язы­ков яв­ля­ет­ся ог­ра­ни­чи­ва­ние бло­ков ко­да фи­гур­ны­ми скоб­ка­ми, в от­ли­чии от, нап­ри­мер, Ba­sic, в ко­то­ром все окон­ча­ния обоз­на­ча­ют­ся как End что-то (Sub, If и пр.). Ну и еще од­на осо­бен­ность - лю­бовь к сок­ра­ще­ни­ям все­го че­го толь­ко мож­но.
    В .NET вце­лом по­яви­лось нес­коль­ко осо­бен­нос­тей - нап­ри­мер, из­чез­ли ос­нов­ные ти­пы дан­ных. Те­перь клю­че­вые сло­ва do­ub­le и int не пред­с­тав­ля­ют со­бой пос­ле­до­ва­тель­ность байт в па­мя­ти, а яв­ля­ют­ся крат­кой за­писью для System.Do­ub­le и System.Int32 (на 32-х раз­ряд­ных ма­ши­нах) со­от­вет­с­т­вен­но. По­яви­лись ин­тер­фей­сы, поз­во­ля­ющие опи­сать на­бор фун­к­ций­, не­об­хо­ди­мых для сов­мес­ти­мос­ти с ка­ким-ли­бо клас­сом, и еще мно­го че­го, в ос­нов­ном не нуж­но­го но­вич­кам.
    И не на­до за­бы­вать, что в .NET сох­ра­не­на сис­те­ма win­dows со­об­ще­ний и со­бы­тий. Так что иметь пред­с­тав­ле­ние о том, как ра­бо­та­ет Win­dows опе­ра­ци­он­ка не­об­хо­ди­мо хо­тя бы на на­чаль­ном уров­не.
    
    Основы ра­бо­ты сис­те­мы
    Специалистов за­ра­нее про­шу не кри­вит­ся - я бу­ду пи­сать толь­ко о са­мых ос­но­вах и прос­тым язы­ком. То что нас ин­те­ре­су­ет - это сис­те­ма со­бы­тий (event) и их кон­т­ро­ля. Что бы ни про­изош­ло в ком­пе - дер­ну­лась мыш­ка, на­жа­ли кла­ви­шу или от­к­ры­ли ок­но - это прев­ра­ща­ет­ся в event. Event'ы бы­ва­ют раз­ные - на все слу­чаи жиз­ни. При же­ла­нии вам ник­то не ме­ша­ет сде­ла­ет свое со­бы­тие, впро­чем это до­воль­но ред­ко нуж­но, хо­тя уметь по­лез­но. Ког­да Event соз­дан - он рас­сы­ла­ет­ся всем за­ин­те­ре­со­ва­ным ли­цам :), сво­его ро­да рас­сыл­ка по ин­те­ре­сам. Ва­ша прог­рам­ма, ес­ли она хо­чет по­лу­чать ин­фор­ма­цию о ка­ких-то со­бы­ти­ях - дол­ж­на под­пи­сать­ся на них. Все до­воль­но прос­то:
    Если вам на­до что­бы ва­ша прог­рам­ма по­лу­ча­ла ин­фор­ма­цию о со­бы­тии на­жа­тия кноп­ки - под­пи­ши­тесь:
    
    кнопка1.Click += new Even­t­Han­d­ler(на­жа­лиК­ноп­ку);
    Этой строч­кой вы вклю­ча­ете фун­к­цию "на­жа­лиК­ноп­ку" в спи­сок рас­сыл­ки ин­фор­ма­ции о со­бы­тии "Click" (на­жа­тие) для кноп­ки "кноп­ка1". Пом­нить на­до толь­ко об од­ном - лю­бые вла­дель­цы рас­сы­лок лю­ди хит­рые, кон­ку­рен­тов не лю­бят и рас­сы­ла­ют толь­ко "сво­им", что в пе­ре­во­де на прог­рам­ми­ро­ва­ние зна­чит - фун­к­ция, ко­то­рую вы вно­си­те в спи­сок дол­ж­на быть имен­но та­ко­го ви­да, ка­ко­го ждут в спис­ке, ина­че не при­мут. Пос­лед­ний мо­мент - Even­t­Han­d­ler - это пос­ред­ник меж­ду спис­ком рас­сыл­ки и по­лу­ча­те­лем, имен­но он дер­жит имя фун­к­ции, ко­то­рой на­до со­об­щить о со­бы­тии. Even­t­Han­d­ler'ов столь­ко же, сколь­ко и раз­ных event'ов, и он то­же дол­жен быть имен­но то­го ти­па, ко­то­рый ждут.
    
    Как чи­тать чу­жой код
    Конечно код бы­ва­ет раз­ный­, и что­бы чи­тать чу­жой код, ко­то­рый на­пи­сан на сла­боз­на­ко­мом язы­ке, без ком­мен­та­ри­ев и в неп­ри­выч­ной ма­не­ре нуж­но не толь­ко уме­ние, но и из­ряд­ная до­ля уда­чи и ин­ту­иции. Од­на­ко ес­ли код до­воль­но прос­той и на­пи­сан с ком­мен­та­ри­ями, хо­тя бы с ка­ки­ми-то, то про­чи­тать его проб­лем обыч­но не сос­тав­ля­ет. Боль­шой плюс C# в этом от­но­ше­нии, это воз­мож­ность вбить не­по­нят­ную строч­ку в Go­og­le или MSDN и поч­ти на­вер­ня­ка най­дет­ся при­мер с под­роб­ным опи­са­ни­ем, или что-то по­доб­ное. Боль­ше чем про C#, сре­ди .NET язы­ков, в се­ти толь­ко про VB на­пи­са­но.
    Итак, раз­бор ко­да. Рас­смот­рим код внут­ри фун­к­ции, так как все ос­таль­ное обыч­но воп­ро­сов не вы­зы­ва­ет.
    Код, как при­ви­ло, струк­ту­ри­ро­ван - или фор­ма­ти­ро­ван та­бу­ля­ци­ей­, или как-то ина­че (нап­ри­мер re­gi­on'ами). Очень час­то каж­дый блок име­ет ком­мен­та­рий­, опи­сы­ва­ющий про­из­во­ди­мую внут­ри опе­ра­цию - нап­ри­мер, "Чте­ние фай­ла", "За­пол­не­ние таб­ли­цы", "Под­бор раз­ме­ров". Та­ким об­ра­зом, мы зна­ет что там про­ис­хо­дит, ос­та­лось по­нять как. Боль­шин­с­т­во, т.е. поч­ти все, фун­к­ции и свой­ст­ва стан­дар­т­ных клас­сов об­ла­да­ют наз­ва­ни­ями точ­но опи­сы­ва­ющи­ми их дей­ст­вия. Ес­ли вы не зна­ете ан­г­лий­ский - поп­ро­буй­те сна­ча­ла пе­ре­вес­ти наз­ва­ние фун­к­ции, раз­бив его на от­дель­ные сло­ва по боль­шим бук­вам: Get­C­hil­d­F­rom­Po­int = Get Child From Po­int = По­лу­чить Ре­бен­ка Из Точ­ки - по­лу­чить до­чер­ний эле­мент уп­рав­ле­ния, на­хо­дя­щий­ся в точ­ке. Дру­гой ва­ри­ант - пе­ре­во­дить имя клас­са и фун­к­цию: Con­vert.ToS­t­ring = Con­vert To String = Пе­ре­вес­ти В Стро­ку. Не­ко­то­рые тер­ми­ны, ко­неч­но, при­дет­ся по­ис­кать в се­ти и пос­ле пе­ре­во­да. А не­ко­то­рые пе­ре­вес­ти по­нят­но не по­лу­чит­ся, по­это­му при­дет­ся по­нять, что имен­но фун­к­ция де­ла­ет. Нап­ри­мер, в та­кой вот строч­ке:
    
    largerImagesComboBox.DataSource = Enum.Get­Na­mes(type­of(Des­k­top­Bac­k­g­ro­un­d­S­t­y­le));
    
    даже ес­ли вы по­ня­тия не име­ете что та­кое Enum, мож­но мно­гое по­нять из прос­то­го пе­ре­во­да: объ­ект Com­bo­Box (вы­па­да­ющее ок­но), его свой­ст­во Ис­точ­ник Дан­ных (Da­ta­So­ur­ce) при­рав­ни­ва­ет­ся че­му-то не­по­нят­но­му. Ло­гич­но пред­по­ло­жить, что ис­точ­ник дан­ных ука­зы­ва­ет на ка­ко­го-ли­бо ви­да спи­сок зна­че­ний. Фун­к­ция, пред­по­ло­жим не­из­вес­т­но­го, клас­са Enum на­зы­ва­ет­ся Get­Na­mes - по­лу­чить име­на, воз­в­ра­ща­ет она мас­сив строк, ко­то­рый и за­пол­нит вы­па­да­ющее ок­но зна­че­ни­ями.
    
    Подведем ито­ги - ни­че­го тол­ком по­лез­но­го я не ска­зал. Все мои со­ве­ты мож­но свес­ти к трем фра­зам:
    1. вни­ма­тель­но чи­тай­те код
    2. учи­те ан­г­лий­ский­, кто не зна­ет
    3. поль­зуй­тесь справ­кой (MSDN) и Go­og­le'ом.
    
Гла­ва 2. Син­так­сис дек­ла­ра­ций.
    
    Вторая по­пыт­ка на­пи­сать что-то по­лез­ное для сов­сем на­чи­на­ющих в С#. 
    Чуть-чуть о струк­ту­ре ко­да: вер­х­ним эле­мен­том струк­ту­ры яв­ля­ют­ся прос­т­ран­с­т­ва имен (na­mes­pa­ce), до­воль­но фик­тив­ная вешь, приз­ван­ная упо­ря­до­чить ту ку­чу клас­сов, спис­ков, ин­тер­фей­сов и кон­с­тант, ко­то­рые уже су­щес­т­ву­ют и бу­дут соз­да­вать­ся. В na­mes­pa­ce мо­гут вхо­дить дру­гие na­mes­pa­ce, клас­сы, enum, кон­с­тан­ты и ин­тер­фей­сы. Класс сос­то­ит из фун­к­ций­, пе­ре­мен­ных и кон­с­тант. По по­во­ду пе­ре­мен­ных - они мо­гут быть объ­яв­ле­ны на лю­бом уров­не, и су­щес­т­во­вать бу­дут толь­ко в пре­де­лах (и во вре­мя жиз­ни) то­го бло­ка, в ко­то­ром объ­яв­ле­ны. Нап­ри­мер: пе­ре­мен­ная объ­яв­лен­ная в клас­се дос­туп­на для все­го клас­са, час­то для дру­гих клас­сов, жи­вет все вре­мя, по­ка жив класс (если он ста­тич­ный­) или объ­ект клас­са (если не ста­тич­ный­). Дру­гой при­мер: пе­ре­мен­ная объ­яв­лен­ная в пре­де­лах бло­ка if { }, ко­то­рый на­хо­дит­ся внут­ри бло­ка for { }, ко­то­рый на­хо­дит­ся внут­ри фун­к­ции клас­са... та­кая пе­ре­мен­ная бу­дет дос­туп­на толь­ко в пре­де­лах бло­ка if {}, в ко­то­ром объ­яв­ле­на и бу­дет жить по­ка не за­кон­чит­ся вы­пол­не­ние бло­ка, т.е. да­же до кон­ца фун­к­ции не до­жи­вет.
    О том, что та­кое ста­ти­чес­кий класс, что зна­чит пе­ре­мен­ная объ­яв­ле­на и пр - чи­тай­те ни­же.
    
    Синтаксис дек­ла­ра­ций
    Декларации - или объ­яв­ле­ния - это оп­ре­де­ле­ние име­ни и фор­ми­ро­ва­ние ти­па, прик­реп­ля­емо­го к это­му име­ни. Т.е. ес­ли вам нуж­но мес­то для хра­не­ния це­ло­го чис­ла, вы дол­ж­ны объ­явить пе­ре­мен­ную, с по­нят­ным вам име­нем, ти­па дан­ных це­лое (int). К сло­ву, int = in­te­ger = це­лое чис­ло. И пом­ни­те, дек­ла­ра­ция не соз­да­ет объ­ект и не вы­де­ля­ет па­мять - это толь­ко зак­реп­ле­ние име­ни за ти­пом. Для соз­да­ния дек­ла­ри­ро­ван­но­го объ­ек­та не­об­хо­ди­мо его ини­ци­али­зи­ро­вать (или оп­ре­де­лить), т.е. ли­бо прис­во­ить на­чаль­ное зна­че­ние, ли­бо за­пус­тить кон­с­т­рук­тор.
    Декларация клас­сов, фун­к­ций и пе­ре­мен­ных на уров­не клас­са тре­бу­ет ука­за­ния мо­ди­фи­ка­то­ра дос­ту­па. Впро­чем, ес­ли не ука­зы­вать бу­дет ис­поль­зо­ван стан­дар­т­ный­, обыч­но pri­va­te. Мо­ди­фи­ка­то­ры дос­ту­па - это клю­че­вые сло­ва, оп­ре­де­ля­ющие, от­ку­да мож­но бу­дет по­лу­чить дос­туп к дек­ла­ри­ро­ван­ной пе­ре­мен­ной­/фун­к­ции и пр. Их все­го нес­коль­ко:
    private - дос­туп воз­мо­жен толь­ко из­нут­ри клас­са, в ко­то­ром объ­яв­ле­на.
    protected - дос­туп воз­мо­жен из это­го клас­са и всех нас­лед­ни­ков.
    internal - дос­туп воз­мо­жен из всей as­sembly (сбор­ки/биб­ли­оте­ки, фай­ла ко­ро­че). Так­же при­ме­ня­ет­ся для клас­сов, enum и ин­тер­фей­сов.
    public - дос­туп воз­мо­жен от­ку­да угод­но. Так­же при­ме­ня­ет­ся для клас­сов, enum и ин­тер­фей­сов.
    Есть еще мо­ди­фи­ка­то­ры сос­то­яния, оп­ре­де­ля­ющие прин­ци­пи­аль­ное сос­то­яние фун­к­ций­:
    static - фун­к­ция ста­тич­на, т.е. воз­мож­но ее ис­пол­не­ние без соз­да­ния объ­ек­та клас­са. Нап­ри­мер, фун­к­ции клас­са System.Math - ста­тич­ные, пос­коль­ку вы­пол­ня­ют­ся прос­тым вы­зо­вом, без соз­да­ния объ­ек­та клас­са Math. Так­же при­ме­ня­ет­ся для клас­сов, тог­да все вхо­дя­щие в не­го фун­к­ции и пе­ре­мен­ные дол­ж­ны быть sta­tic.
    abstract - фун­к­ция толь­ко дек­ла­ри­ро­ва­на, а оп­ре­де­ле­ние фун­к­ции дол­ж­но быть в клас­сах нас­лед­ни­ках. Ис­поль­зу­ет­ся для соз­да­ния шаб­ло­на клас­са, все нас­лед­ни­ки ко­то­ро­го обя­за­тель­но име­ют не­кий на­бор фун­к­ций. Мо­жет су­щес­т­во­вать толь­ко в ab­s­t­ract клас­се. Так­же при­ме­ня­ет­ся для клас­сов.
    virtual - фун­к­цию мож­но пе­ре­оп­ре­де­лить в клас­се нас­лед­ни­ке.
    override - ис­поль­зу­ет­ся для ука­за­ния, что опи­сы­ва­емая фун­к­ция пе­ре­оп­ре­де­ля­ет ро­ди­тель­с­кую.
    Модификатор дос­ту­па для фун­к­ции/пе­ре­мен­ной внут­ри клас­са не мо­жет быть бо­лее дос­туп­ным, не­же­ли мо­ди­фи­ка­тор дос­ту­па все­го клас­са.
    Переменные, дек­ла­ри­ру­емые внут­ри фун­к­ций мо­ди­фи­ка­то­ра дос­ту­па не тре­бу­ют.
    
    Декларация пе­ре­мен­ных обя­за­тель­но сос­то­ит из ти­па дан­ных и име­ни, ос­таль­ное - по си­ту­ации и же­ла­нию.
    Примеры дек­ла­ра­ций пе­ре­мен­ных:
    
    int i;
    public do­ub­le myDo­ub­le1, myDo­ub­le2;
    internal sta­tic System.String con­st_string = "кон­с­тан­та";
    
    Декларация фун­к­ций обя­за­тель­но сос­то­ит из ти­па воз­в­ра­ща­емых дан­ных, име­ни и спис­ка ар­гу­мен­тов, ос­таль­ное - по си­ту­ации. Воз­ра­ща­емым ти­пом дан­ных мо­жет быть vo­id - пус­той - т.е. нет воз­в­ра­ща­емых дан­ных. Спи­сок ар­гу­мен­тов то­же мо­жет быть пус­тым. Фун­к­ции, в от­ли­чии от пе­ре­мен­ных, не мо­гут быть толь­ко дек­ла­ри­ро­ва­ны - они обя­за­тель­но сра­зу оп­ре­де­ля­ют­ся. Един­с­т­вен­ное ис­к­лю­че­ние - тип фун­к­ций ab­s­t­ract, они толь­ко дек­ла­ри­ру­ют­ся, а оп­ре­де­ля­ют­ся уже в клас­сах нас­лед­ни­ках.
    Примеры дек­ла­ра­ций и оп­ре­де­ле­ния фун­к­ций­:
    
    private vo­id MyFunc1(int i1, do­ub­le d1) {}
    internal ab­s­t­ract string Ge­tAs­S­t­ring();
    internal over­ri­de string Ge­tAs­S­t­ring() {}
    public sta­tic do­ub­le Get­Sum(do­ub­le d1, do­ub­le d2) { re­turn d1+d2; }
    
    Декларация клас­са мо­жет вклю­чать ука­за­ние клас­са ро­ди­те­ля, че­рез дво­ето­чие, да­лее че­рез за­пя­тую - под­к­лю­чен­ные ин­тер­фей­сы. Ин­тер­фей­с - спи­сок фун­к­ций. В ка­ком-то кри­вом смыс­ле - ро­ди­тель­с­кий класс, ко­то­рый мож­но про­из­воль­но при­ле­пить ку­да угод­но. Та­ким об­ра­зом по­лу­ча­ют­ся клас­сы, от раз­ных ро­ди­те­лей­, но под­дер­жи­ва­ющие од­ни и те же опе­ра­ции. До­воль­но удоб­ная вещь.
    Пример дек­ла­ра­ции клас­са:
    
    internal class MyClass1 : MyPa­ren­t­C­lass1, IIn­ter­fa­ce1, IIn­ter­fa­ce2 {
    }
    
    Декларация кон­с­т­рук­то­ра клас­са мо­жет вклю­чать ука­за­ние на дру­гой кон­с­т­рук­тор, ко­то­рый дол­жен быть за­пу­щен до дек­ла­ри­ру­емо­го, нап­ри­мер, кон­с­т­рук­тор ро­ди­тель­с­ко­го клас­са:
    
    public class MyClass : Pa­ren­t­C­lass {
    public MyClass(int i1) : ba­se() {
    }
    public MyClass() : this(0) {
    }
    }
    
    И пос­лед­нее - дек­ла­ра­ция Enum. Enum = enu­me­ra­ti­on = пе­ре­чис­ле­ние. Спи­сок зна­че­ний. Соз­да­ет­ся для двух це­лей­: его мож­но ис­поль­зо­вать прог­рам­мис­ту - свя­зать спи­сок це­ло­чис­лен­ных зна­че­ний с по­нят­ны­ми сло­ва­ми, и поль­зо­вать­ся сло­ва­ми, а не чис­ла­ми, в ко­то­рых лег­ко за­пу­тать­ся; его мож­но ис­поль­зо­вать поль­зо­ва­те­лям - име­на зна­че­ний в спис­ке мож­но лег­ко вы­вес­ти на поль­зо­ва­те­ля.
    Пример дек­ла­ра­ции Enum:
    
    public enum Ima­ge­Fi­le­For­mat {
    JPEG,
    PNG,
    TIFF,
    Bitmap}
    public enum Sup­por­ted­Lan­gu­ages {
    Русский=1,
    English=10,
    German=11}
    
Гла­ва 3. Син­так­сис дек­ла­ра­ций мас­си­вов, свойств, де­ле­га­тов. As­sembly.
    
    
    Массивы
    Итак, мас­сив - на­бор объ­ек­тов од­но­го ти­па, име­ющий чет­кую пос­ле­до­ва­тель­ность этих объ­ек­тов и стро­го за­дан­ный раз­мер, т.е. ко­ли­чес­т­во объ­ек­тов в на­бо­ре. Мас­сив мо­жет быть лю­бо­го ти­па, раз­мер мас­си­ва дол­жен быть боль­ше 0 и мень­ше чем мак­си­маль­ное зна­че­ние int (на 32 раз­ряд­ных ма­ши­нах - 2^32=4294967296).
    Декларация мас­си­ва по­доб­на дек­ла­ра­ции пе­ре­мен­ной­, с од­ним толь­ко из­ме­не­ни­ем - пос­ле ти­па дек­ла­ри­ру­емо­го мас­си­ва до­бав­ля­ют­ся квад­рат­ные скоб­ки - [].
    Пример:
    
    int[] in­tAr­ray;
    В строч­ке вы­ше по­ка­за­на дек­ла­ра­ция од­но­мер­но­го мас­си­ва ти­па int. Мас­сив мо­жет быть мно­го­мер­ным. При­чем в .NET мно­го­мер­ность мо­жет быть раз­ной.
    Вариант 1 - мно­го­мер­ный мас­сив ста­ро­го об­раз­ца, так на­зы­ва­емый мас­сив-мас­си­вов:
    
    int[][] int2dAr­ray;
    Система прос­тая - каж­дая скоб­ка соз­да­ет свой мас­сив. Т.е. мож­но пред­с­та­вить это в та­ком ви­де: (int[])[] ar­ray­Na­me - мас­сив ти­па int[], ко­то­рый то­же яв­ля­ет­ся мас­си­вом. По­доб­ных вло­же­ний мо­жет быть нес­коль­ко.
    Вариант 2 - мно­го­мер­ный мас­сив .NET:
    
    int[,] int2dAr­ray;
    
    Внутри скоб­ки ста­вит­ся нуж­ное ко­ли­чес­т­во за­пя­тых, каж­дая за­пя­тая соз­да­ет плюс од­но из­ме­ре­ние мас­си­ва. Т.е. в при­ве­ден­ной вы­ше строч­ке дек­ла­ри­ру­ет­ся таб­ли­ца (дву­мер­ный мас­сив) для це­ло­чис­лен­ных дан­ных.
    У каж­до­го ва­ри­ан­та есть свои плю­сы и свои ми­ну­сы. Ес­ли ко­рот­ко - вто­рой ва­ри­ант удоб­нее и чуть быс­т­рее ра­бо­та­ет, за­то пер­вый ва­ри­ант поз­во­ля­ет раз­де­лять мас­сив на сос­тав­ля­ющие при не­об­хо­ди­мос­ти. При­ме­ры ис­поль­зо­ва­ния мас­си­вов бу­дут по­том.
    
    Свойства
    Свойства - спе­ци­аль­ная фи­ча для воз­мож­нос­ти про­вер­ки дан­ных, вво­ди­мых в пе­ре­мен­ную и за­да­ния ка­ких-ли­бо дей­ст­вий при из­ме­не­нии зна­че­ния. Сво­его ро­да пос­ред­ник меж­ду пе­ре­мен­ной и про­чим ко­дом, а по сов­мес­ти­тель­с­т­ву - ох­ран­ник и сек­ре­тарь этой пе­ре­мен­ной :). Свой­ст­ва су­щес­т­ву­ют толь­ко внут­ри клас­са, вмес­те с пе­ре­мен­ны­ми уров­ня клас­са.
    Задаются очень прос­то:
    
    private flo­at _si­ze; //пе­ре­мен­ная
    public pro­perty flo­at Si­ze { //свой­ст­во - пос­ред­ник пе­ре­мен­ной _si­ze
    get {
    return _si­ze;
    }
    set {
    //проверить на пра­виль­ность
    _size = va­lue;
    OnSizeChanged();
    }
    }
    Сначала, как обыч­но, мо­ди­фи­ка­тор дос­ту­па. Клю­че­вое сло­во pro­perty ука­зы­ва­ет, что дек­ла­ри­ру­ет­ся свой­ст­во. Да­лее ука­зы­ва­ет­ся тип дан­ных, в при­ме­ре - оди­нар­ное дроб­ное. За­тем имя свой­ст­ва и от­к­ры­ва­ет­ся фи­гур­ная скоб­ка, на­чи­на­ющая блок опи­са­ния. Лю­бое свой­ст­во дол­ж­но иметь блок get - по­лу­чить. Ес­ли свой­ст­во толь­ко-для-чте­ния, оно не име­ет бло­ка set - ус­та­но­вить. В каж­дом бло­ке про­пи­сы­ва­ет­ся, что на­до сде­лать. Пом­ни­те, что свой­ст­во не хра­нит дан­ные, т.к. не яв­ля­ет­ся пе­ре­мен­ной - оно лишь пос­ред­ник, по­это­му в бло­ке get ука­за­но - вер­нуть зна­че­ние пе­ре­мен­ной _si­ze, в ко­то­рой зна­че­ние хра­нит­ся ре­аль­но. В бло­ке set на­пи­са­но, что де­лать при ус­та­нов­ке но­во­го зна­че­ния - сна­ча­ла про­ве­рить на пра­виль­ность... ну, нап­ри­мер, что­бы вво­ди­ли от 0 до 1, ког­да нуж­но и т.п. Пос­ле про­вер­ки - ус­та­но­вить зна­че­ние; клю­че­вое сло­во va­lue, в дан­ном слу­чае, ука­зы­ва­ет на вхо­дя­щее зна­че­ние. Ну и под за­на­вес - вы­пол­нить со­бы­тие "раз­мер из­ме­нил­ся". За­вер­ша­ет дек­ла­ра­цию зак­ры­ва­юща­яся фи­гур­ная скоб­ка.
    Свойства поз­во­ля­ют соз­да­вать все пе­ре­мен­ные как pri­va­te, а нуж­ные для от­к­ры­то­го дос­ту­па вы­во­дить че­рез свой­ст­ва, имея та­ким об­ра­зом га­ран­тию, что лю­бое внеш­нее из­ме­не­ние зна­че­ния бу­дет про­ве­ре­но на пра­виль­ность.
    
    Делегаты
    Делегаты - ес­ли прос­то, то это ука­за­те­ли на фун­к­ции. Ес­ли вам на­до пе­ре­дать фун­к­цию как ар­гу­мент, то у вас два пу­ти - ли­бо че­рез стро­ко­вое наз­ва­ние фун­к­ции ис­кать ее в биб­ли­оте­ке (assembly) и вы­зы­вать как внеш­нюю фун­к­цию, ли­бо ис­поль­зо­вать де­ле­гат. Де­ле­гат поз­во­ля­ет оп­ре­де­лить под­пись фун­к­ции - т.е. воз­в­ра­ща­емый тип, ти­пы и ко­ли­чес­т­во ар­гу­мен­тов.
    Делегат - это объ­ект уров­ня клас­са, соб­с­т­вен­но это и есть класс, толь­ко очень осо­бый. Од­на­ко дек­ла­ра­ция его очень по­хо­жа на дек­ла­ра­цию аб­с­т­рак­т­ной фун­к­ции:
    
    public de­le­ga­te do­ub­le MyDe­le­ga­te(do­ub­le d1, do­ub­le d2);
    Не за­бы­вай­те, что де­ле­гат - это класс, и для то­го, что­бы его мож­но бы­ло ис­поль­зо­вать, не­об­хо­ди­мо соз­дать объ­ект де­ле­га­та, ко­то­рый при­пи­сы­ва­ет­ся к кон­к­рет­ной фун­к­ции и даль­ше пе­ре­да­ет­ся, ес­ли ну­жен. У де­ле­га­та есть ме­тод In­vo­ke, ко­то­рый соб­с­т­вен­но и за­пус­ка­ет фун­к­цию, на ко­то­рую де­ле­гат ука­зы­ва­ет.
    Пример ис­поль­зо­ва­ния де­ле­га­та внут­ри фун­к­ции:
    
    MyDelegate md = new MyDe­le­ga­te(MyFun­c­ti­on);
    md.Invoke(d1, d2);
    В при­ме­ре пред­по­ла­га­ет­ся, что d1 и d2 - пе­ре­мен­ные ти­па do­ub­le, a фун­к­ция MyFun­c­ti­on име­ет под­пись: do­ub­le MyFun­c­ti­on(do­ub­le d1, do­ub­le d2).
    
    Assembly
    Кратко: As­sembly = сбор­ка, файл, яв­ля­ющий­ся кон­тей­не­ром для na­mes­pa­ce/клас­сов/кон­с­тант/де­ле­га­тов/интер­фей­сов и ре­сур­сов (кар­ти­нок/тек­с­та/ико­нок и пр.). As­sembly мо­жет быть за­пус­к­ным (exe) или не за­пус­к­ным (dll). От­ли­чие толь­ко в том, что в за­пус­к­ном фай­ле про­пи­сан ме­тод Win­Ma­in (или ma­in для кон­соль­ных при­ло­же­ний­). Нес­мот­ря на рас­ши­ре­ния фай­ла, ни­че­го об­ще­го (кро­ме поль­зо­ва­тель­с­ко­го при­ме­не­ния) со ста­ры­ми (до .NET) фай­ла­ми не име­ет. Ос­нов­ные два от­ли­чия от win32 стан­дар­тов - as­sembly име­ет соб­с­т­вен­ное опи­са­ние внут­ри се­бя. Опи­са­ние вклю­ча­ет: вер­сию, ав­то­ра, па­ра­мет­ры бе­зо­пас­нос­ти не­об­хо­ди­мые для вы­пол­не­ния и мно­гое дру­гое, в час­т­нос­ти - опи­са­ние под­пи­сей всех фун­к­ций­, пе­ре­мен­ных и т.д. Т.е. не на­до знать за­ра­нее как вы­зы­вать ту или иную фун­к­цию из биб­ли­оте­ки - это мож­но уз­нать на ле­ту. Вто­рое от­ли­чие - as­sembly хра­нит код не в ви­де ас­сем­б­ле­ра, а в MSIL - Mic­ro­soft In­ter­me­di­ate Lan­gu­age - сво­е­об­раз­ный ме­та-язык, ко­то­рый ком­пи­ли­ру­ет­ся на ле­ту и по зап­ро­су (just-in-ti­me com­pi­ling and de­bug­ging) в со­от­вет­с­т­вии с тре­бо­ва­ни­ями сис­те­мы на ко­то­рой ком­пи­ли­ру­ет­ся (раз­ряд­ность про­цес­со­ра и т.д.). Плю­сы та­ко­го под­хо­да - кросс-плат­фор­мен­ность и кросс-сис­тем­ность, а так­же бе­зо­пас­ность вы­пол­не­ния. Ми­ну­сы - код всег­да от­к­рыт и дос­ту­пен лю­бо­му для проч­те­ния, а так­же ухо­дит вре­мя и ре­сур­сы на ком­пи­ля­цию. Впро­чем, кни­ги и му­зы­ку то­же мо­жет лю­бой про­чи­тать/услы­шать... и по­че­му-то ав­то­ров это не нап­ря­га­ет, а пла­ги­ато­ров не так уж мно­го...
    Assembly мо­жет быть од­но­мо­дуль­ным и мно­го­мо­дуль­ным. Мо­дуль - часть as­sembly, как пра­ви­ло, до сбор­ки пред­с­тав­ля­ющая со­бой от­дель­ный файл. Т.е. нес­коль­ко биб­ли­отек клас­сов мож­но объ­еди­нить в од­ну as­sembly, и до­ба­вить еще кар­тин­ки до ку­чи. Каж­дая кар­тин­ка и каж­дая биб­ли­оте­ка бу­дут от­дель­ным мо­ду­лем внут­ри еди­ной as­sembly. Плю­сов в та­ком под­хо­де ма­ло­ва­то, по­это­му ред­ко ис­поль­зу­ет­ся. MS Vi­su­al Stu­dio да­же не име­ет воз­мож­нос­ти соз­да­вать муль­ти­мо­дуль­ные as­sembly в ин­тер­фей­сном ре­жи­ме.
    Лично я от­но­шусь к as­sembly по ста­ро­му - как к exe и dll win32 стан­дар­та. Един­с­т­вен­ное, о чем при­хо­дит­ся пом­нить - as­sembly дол­ж­ны иметь иден­ти­фи­ка­то­ры, ес­ли ими со­би­ра­етесь поль­зо­вать­ся не вы один. Т.е. вер­сия, ав­тор и раз­ре­ше­ния бе­зо­пас­нос­ти дол­ж­ны быть про­пи­са­ны. Да и под­пи­сать as­sembly клю­чом strong na­me то­же нуж­но. Ключ strong na­me - уни­каль­ный иден­ти­фи­ка­тор as­sembly, ко­то­рый ис­поль­зу­ет­ся для рас­поз­на­ва­ния раз­ных as­sembly с од­ним име­нем на од­ной ма­ши­не. Для под­пи­сы­ва­ния есть кон­соль­ная ути­ли­та sn.exe, а в VS2005 есть и ин­тер­фей­сные оп­ции в свой­ст­вах про­ек­та для этой це­ли.
    Если кто-то хо­чет ра­зоб­рать­ся под­роб­нее - есть неп­ло­хая статья (ку­сок кни­ги) на co­dep­ro­j­ect: лежит здесь - http://www.codeproject.com/books/1893115593_6.asp.
    
Глава 4. Основные операторы и конструкты.
    
    Рассмотрев син­так­сис дек­ла­ра­ций­, пе­ре­хо­дим к ос­нов­ным кон­с­т­рук­там или ко­ман­дам язы­ка. Как из­вес­т­но, код внут­ри фун­к­ции вы­пол­ня­ет­ся пос­т­роч­но, ес­ли не ис­поль­зу­ют­ся ка­кие-ли­бо ко­ман­ды. Дру­ги­ми сло­ва­ми - ко­ман­ды язы­ка приз­ва­ны обес­пе­чить не­ли­ней­ность вы­пол­не­ния ко­да. Са­мая из­вес­т­ная из не-ли­ней­ных ко­манд - go­to - есть в язы­ке C#, од­на­ко я нас­то­ятель­но не ре­ко­мен­дую ей поль­зо­вать­ся. Ос­таль­ных ко­манд впол­не дос­та­точ­но, что­бы обес­пе­чить лю­бую не­ли­ней­ность, а при­вы­кать к go­to - это как пра­ви­ло оз­на­ча­ет при­выч­ку к пло­хой струк­ту­ри­за­ции ко­да.
    Но сна­ча­ла, что­бы бы­ло про­ще по­ни­мать ко­ман­ды, рас­смот­рим не­ко­то­рые опе­ра­то­ры язы­ка.
    
    Операторы
    Операторы срав­не­ния:
    ‹ - стро­го мень­ше. Оп­ре­де­лен для лю­бых чис­ло­вых ти­пов (int, do­ub­le, short, byte, de­ci­mal, flo­at, bo­ol)
    › - стро­го боль­ше. Ана­ло­гич­но пре­ды­ду­ще­му.
    ‹= - мень­ше или рав­но. Ана­ло­гич­но пре­ды­ду­ще­му.
    ›= - боль­ше или рав­но. Ана­ло­гич­но пре­ды­ду­ще­му.)
    == - рав­но (экви­ва­лен­т­но). Оп­ре­де­лен для боль­шин­с­т­ва ти­пов и клас­сов. Для мно­гих клас­сов оз­на­ча­ет имен­но иден­тич­ность объ­ек­тов клас­са, а не ра­вен­с­т­во их внут­рен­них зна­че­ний.
    != - не рав­но. Ана­ло­гич­но пре­ды­ду­ще­му.
    Обратите вни­ма­ние: про­вер­ка на ра­вен­с­т­во обоз­на­ча­ет­ся дву­мя зна­ка­ми рав­но! Од­ним зна­ком обоз­на­ча­ет­ся опе­ра­тор прис­во­ения зна­че­ния.
    
    Логические опе­ра­то­ры:
    || - ло­ги­чес­кое ИЛИ.
    && - логическое И.
    ! - ло­ги­чес­кое НЕ.
    
    Арифметические опе­ра­то­ры:
    помимо стан­дар­т­ных +, -, *, / есть их мо­ди­фи­ка­ции со зна­ком рав­но: +=, -=, *=, /=. Оз­на­ча­ют "вы­пол­нить опе­ра­тор и при­рав­нять", т.е.
    
    i += 10;
    означает при­ба­вить 10 к зна­че­нию пе­ре­мен­ной i, и за­пи­сать ре­зуль­тат в нее же. Это ана­ло­гич­но за­пи­си:
    
    i = i + 10;
    есть еще два опе­ра­то­ра:
    ++ - при­ба­вить еди­ни­цу к пе­ре­мен­ной и за­пи­сать ре­зуль­тат в нее. Ин­к­ре­мен­т­ный опе­ра­тор.
    -- - от­нять еди­ни­цу от зна­че­ния пе­ре­мен­ной и за­пи­сать ре­зуль­тат в нее. Дек­ре­мен­т­ный опе­ра­тор.
    
    
    Основные кон­с­т­рук­ты мож­но раз­де­лить на цик­ло­вые - поз­во­ля­ющие за­пус­кать ку­сок ко­да в цик­ле, и ус­лов­ные - поз­во­ля­ющие вы­пол­нять нуж­ный ку­сок ко­да, вы­би­ра­емый по ус­ло­вию. Я опи­шу ос­нов­ные кон­с­т­рук­ты, ес­ли ка­кой за­бу­ду, а вам он ин­те­ре­сен - пи­ши­те.
    
    Циклы
    Самый рас­п­рос­т­ра­нен­ный цикл - for. Смыс­ло­вое пред­наз­на­че­ние - вы­пол­нять пос­ле­до­ва­тель­ность дей­ст­вий за­дан­ное ко­ли­чес­т­во раз. За­да­ет­ся сле­ду­ющим об­ра­зом:
    
    for (i = 0; i ‹ 10; i++) { /*Ваш код здесь*/ }
    После клю­че­во­го сло­ва for сле­ду­ет круг­лая скоб­ка, внут­ри кто­рой обя­за­тель­но при­сут­с­т­ву­ют три вы­ра­же­ния, раз­де­лен­ные точ­кой с за­пя­той. Пер­вое вы­ра­же­ние, в на­шем слу­чае "i=0" - опе­ра­ция, ко­то­рую на­до вы­пол­нить пе­ред на­ча­лом цик­ла. За­час­тую в этом мес­те пи­шут дек­ла­ра­цию пе­ре­мен­ной­, ко­то­рая су­щес­т­ву­ет толь­ко для цик­ла, тог­да вы­ра­же­ние выг­ля­дит, нап­ри­мер так: "int i = 0". С тем же ус­пе­хом по­ле мо­жет быть пус­тым. Вто­рое вы­ра­же­ние ус­та­нав­ли­ва­ет ус­ло­вие, при ко­то­ром цикл дол­жен про­дол­жать­ся... его мож­но рас­смат­ри­вать как "Про­дол­жать цикл по­ка вы­ра­же­ние 2 ис­тин­но". Мо­жет быть пус­тым, но тог­да вам са­мим при­дет­ся внут­ри цик­ла про­ве­рять ус­ло­вие вы­хо­да и са­мим же вы­хо­дить из цик­ла. Третье вы­ра­же­ние за­да­ет опе­ра­цию, ко­то­рую на­до вы­пол­нить по за­вер­ше­нии каж­до­го кру­га цик­ла. То­же мо­жет быть пус­тым.
    Итак в строч­ке при­ме­ра по­ка­зан цикл, об­ну­ля­ющий пе­ре­мен­ную i вна­ча­ле, ис­пол­ня­ющий­ся по­ка пе­ре­мен­ная i мень­ше 10, пос­ле каж­до­го кру­га уве­ли­чи­ва­ющий i на 1, что да­ет 10 вы­пол­не­ний цик­ла, при нор­маль­ных ус­ло­ви­ях.
    Для до­пол­ни­тель­но­го уп­рав­ле­ния вы­пол­не­ни­ем цик­ла пре­дус­мот­ре­ны еще два клю­че­вых сло­ва: con­ti­nue - за­вер­ша­ет те­ку­щий круг вы­пол­не­ния, bre­ak - за­вер­ша­ет цикл.
    
    for (int i = 0; i ‹ 10; i ++) { // цикл на 10 кру­гов
    if (so­me_array[i] == 0) { con­ti­nue;} // ес­ли i-тая ячей­ка мас­си­ва рав­на 0 - пой­ти на след круг
    int res = Do­So­met­hing(so­me_array[i]); // что-то сде­лать с ячей­кой мас­си­ва и вер­нуть зна­че­ние
    if (res == -1) { bre­ak;} // ес­ли вер­ну­лось -1 - прер­вать цикл
    }
    
    Цикл whi­le (быв­ший do...whi­le). Смыс­ло­вое пред­наз­на­че­ние - вы­пол­нять пос­ле­до­ва­тель­ность дей­ст­вий­, по­ка что-то не слу­чит­ся/изме­нит­ся.
    задается так:
    
    while (flag == true) { /*Ваш код здесь*/ }
    После клю­че­во­го сло­ва whi­le сле­ду­ет круг­лая скоб­ка, в ко­то­рой за­да­ет­ся ло­ги­чес­кое вы­ра­же­ние, про­ве­ря­емое на ис­тин­ность каж­дый круг цик­ла. Цикл вы­пол­ня­ет­ся по­ка вы­ра­же­ние ис­тин­но, или до клю­че­во­го сло­ва bre­ak, внут­ри цик­ла. Сло­во con­ti­nue при­ме­ни­мо к это­му цик­лу так же, как и к пре­ды­ду­ще­му.
    
    Цикл fo­re­ach. Смыс­ло­вое пред­наз­на­че­ние - вы­пол­нить ку­сок ко­да для каж­до­го чле­на мас­си­ва/спис­ка/кол­лек­ции. В .NET по­яви­лись клас­сы обес­пе­чи­ва­ющие не­ну­ме­ро­ван­ные, ди­на­ми­чес­кие мас­си­вы. В ос­нов­ном для них и был сде­лан этот опе­ра­тор.
    задается так:
    
    foreach (Class obj­ec­t­Na­me in col­lec­ti­on­Na­me) { /* Ваш код здесь */ }
    После клю­че­во­го сло­ва fo­re­ach сле­ду­ет круг­лая скоб­ка в ко­то­рой за­да­ет­ся класс оди­ноч­но­го объ­ек­та, над ко­то­рым вы­пол­ня­ет­ся опе­ра­ция в ос­нов­ном бло­ке, имя объ­ек­та (пе­ре­мен­ной­) для об­ра­ще­ния в ос­нов­ном бло­ке, за­тем клю­че­вое сло­во in и имя пе­ре­мен­ной­, ука­зы­ва­ющее на мас­сив/спи­сок/кол­лек­цию объ­ек­тов то­го клас­са, ко­то­рый ука­за­ли в на­ча­ле ус­ло­вия. Ос­нов­ной код, как обыч­но, пи­шет­ся в фи­гур­ных скоб­ках.
    Ну, нап­ри­мер, час­то встре­ча­ет­ся - цикл по всем фай­лам ка­ко­го-ли­бо ти­па в ка­та­ло­ге:
    
    DirectoryInfo di = new Di­rec­tor­yIn­fo("C:\\temp"); // соз­дать объ­ект ин­фор­ма­ции о ка­та­ло­ге С:\temp
    foreach (Fi­le­In­fo fi in di.Get­Fi­les("*.txt")) { // для каж­до­го объ­ек­та ти­па Fi­le­In­fo в кол­лек­ции, воз­в­ра­ща­емой фун­к­ци­ей Get­Fi­les
    // что-то сде­лать
    }
    
    Условия
    Условие if...else. Смыс­ло­вое пред­наз­на­че­ние - вы­пол­нить блок ко­да, толь­ко ес­ли ус­ло­вие ис­тин­но... с ва­ри­ан­том - ес­ли лож­но - вы­пол­нить дру­гой ку­сок ко­да.
    задается так:
    
    if (flag == true) { /* Ваш код здесь */ }
    else if (flag2 == true) { /* Ваш код здесь */ }
    else { /* Ваш код здесь */ }
    После клю­че­во­го сло­ва if сле­ду­ет ло­ги­чес­кое ус­ло­вие в круг­лых скоб­ках, при ис­тин­нос­ти ко­то­ро­го вы­пол­ня­ет­ся код, рас­по­ло­жен­ный в фи­гур­ных скоб­ках. Ес­ли ус­ло­вие лож­но - вы­пол­ня­ет­ся сле­ду­ющий блок. Сле­ду­ющий блок дол­жен на­чи­нать­ся с клю­че­во­го сло­ва el­se, за ко­то­рым мо­жет сле­до­вать сло­во if и еще од­но ус­ло­вие. Та­кая це­поч­ка мо­жет быть длин­ной­, но на­до пом­нить, что пос­ле вы­пол­не­ния хо­тя бы од­но­го бло­ка це­поч­ка пре­ры­ва­ет­ся.
    Пример:
    
    bool flag1 = true, flag2 = fal­se;
    if (flag1 && flag2) {Do­So­met­hing(); }
    else if (flag1) { Do­So­met­hing2(); }
    else { Do­So­met­hing3(); }
    В этом при­мер бу­дет вы­пол­не­на толь­ко фун­к­ция Do­So­met­hing2(). В пер­вом ус­ло­вии мы про­ве­ря­ем оба фла­га на ис­тин­ность, ес­ли хо­тя бы один не ра­вен ис­тин­но, пе­ре­хо­дим на вто­рой блок. Вто­рой блок то­же с ус­ло­ви­ем - про­ве­ря­ем пер­вый флаг на ис­тин­ность - мы точ­но зна­ем, что хо­тя бы один флаг ло­жен, ес­ли пер­вый ис­тин­нен - вто­рой ло­жен. Тре­тий блок - ес­ли пер­вый не ис­тин­нен, то ли­бо вто­рой ис­тин­нен, ли­бо оба лож­ны, но воз­мож­но нам это уже не важ­но...
    
    Пример 2:
    
    int i = 1;
    if (i == 0) { Do­So­met­hing1(); }
    else if (i == 1) { Do­So­met­hing2(); }
    else if (i == 2) {Do­So­met­hing3(); }
    else { Do­El­se(); }
    В этом, край­не нег­ра­мот­ном при­ме­ре, то­же бу­дет вы­пол­не­на толь­ко фун­к­ция Do­So­met­hing2(). А гра­мот­но та­кую си­ту­ацию рас­пи­сы­вать че­рез опе­ра­тор switch;
    
    Условие swit­ch...ca­se. Смыс­ло­вое пред­наз­на­че­ние - вы­пол­нять кус­ки ко­да по зна­че­нию пе­ре­мен­ной. Сво­е­об­раз­ная раз­вил­ка (или пе­рек­лю­ча­тель). С до­пол­ни­тель­ным воз­мож­нос­тя­ми.
    задется так:
    
    switch (var) {
    case ‹val1›:
    DoSomething();
    case ‹val2›:
    DoSomething2();
    break;
    default:
    DefaultAction();
    break;
    }
    После клю­че­во­го сло­ва switch сле­ду­ет имя пе­ре­мен­ной­, по чьему зна­че­нию нуж­но раз­вет­вить код. В фи­гур­ных скоб­ках за­да­ют­ся ва­ри­ан­ты зна­че­ний­, ко­то­рые дол­ж­ны об­ра­ба­ты­вать­ся. Ва­ри­ант зна­че­ния за­да­ет­ся как клю­че­вое сло­во ca­se, зна­че­ние пе­ре­мен­ной­, дво­ето­чие. Ес­ли фун­к­ци­ональ­ный блок за­кан­чи­ва­ет­ся сло­вом bre­ak - прог­рам­ма вы­хо­дит из бло­ка switch пос­ле не­го, ес­ли не за­кан­чи­ва­ет­ся - прог­рам­ма пе­ре­хо­дит на вы­пол­не­ние сле­ду­юще­го по по­ряд­ку бло­ка ca­se. клю­че­вое сло­во de­fa­ult на мес­те ca­se оз­на­ча­ет "все ва­ри­ан­ты".
    Пример:
    
    int i = 1;
    switch (i) {
    case 2:
    DoSomething2();
    break;
    case 1:
    DoSomething1();
    case 3:
    DoSomething3();
    break;
    default:
    DoDefault();
    break;
    }
    В этом при­ме­ре бу­дут вы­пол­не­ны фун­к­ции Do­So­met­hing1() и Do­So­met­hing3().
    Переменная мо­жет быть не чис­ло­вой­:
    
    char c = '!';
    switch (c) {
    case '?':
    DoQuestion();
    break;
    case '!':
    DoExclamation();
    break;
    }
    В этом при­ме­ре бу­дет вы­пол­не­на толь­ко фун­к­ция Do­Ex­c­la­ma­ti­on(), а ес­ли пе­ре­мен­ная с бу­дет рав­на не воп­ро­си­тель­но­му зна­ку и не вос­к­ли­ца­тель­но­му - ни­че­го сде­ла­но не бу­дет.
    
    По по­во­ду на­хож­де­ния ос­нов­но­го ко­да внут­ри фи­гур­ных ско­бок - ес­ли код сос­то­ит из од­ной строч­ки, мож­но пи­сать его без фи­гур­ных ско­бок:
    
    if (flag == true) re­turn;
    
Гла­ва 5. Ини­ци­али­за­ция пе­ре­мен­ных. Кол­лек­ции. Ти­пы дан­ных.
    
    
    Инициализация.
    Я уже упо­ми­нал это сло­во, в та­ком при­мер­но кон­тек­с­те - Пос­ле дек­ла­ра­ции пе­ре­мен­ной­, пе­ред ее ис­поль­зо­ва­ни­ем, не­об­хо­ди­мо эту пе­ре­мен­ную ин­ци­али­зи­ро­вать. Поп­ро­бу­ем ра­зоб­рать­ся точ­нее: ини­ци­али­за­ция - про­цесс вы­де­ле­ния па­мя­ти под пе­ре­мен­ную и за­пол­не­ние этой па­мя­ти зна­че­ни­ями по умол­ча­нию. Зна­че­ния по умол­ча­нию - это, поч­ти всег­да, раз­ные фор­мы ну­ля: для int - 0, для do­ub­le - 0.0, для string - "", для bo­ol - fal­se и т. д.
    Та часть, ко­то­рая ка­са­ет­ся вы­де­ле­ния па­мя­ти, это те­перь де­ло ком­пи­ля­то­ра и к прог­рам­мис­ту поч­ти не име­ет ни­ка­ко­го от­но­ше­ния. С точ­ки зре­ния прог­рам­мис­та, ини­ци­али­за­ция - пер­вое прис­во­ение зна­че­ния пе­ре­мен­ной.
    Есть та­кая тон­кость - пе­ре­мен­ные, объ­яв­лен­ные на уров­не клас­са ини­ци­али­зи­ру­ют­ся ав­то­ма­ти­чес­ки при соз­да­нии объ­ек­та клас­са. Но это от­но­сит­ся толь­ко к ос­нов­ным ти­пам дан­ных (int, do­ub­le, bo­ol, byte и т. д.) кро­ме string. Все пе­ре­мен­ные клас­сов по­лу­ча­ют при ав­то­ма­ти­чес­кой ини­ци­али­за­ции зна­че­ние null и string то­же.
    Комментарий для спе­ци­алис­тов: ес­ли быть точ­ным, то null - это зна­че­ние по умол­ча­нию для всех пе­ре­мен­ных клас­сов Mar­s­hal­B­y­Re­fe­ren­ce - об­ра­ба­ты­ва­емых че­рез ука­за­тель. Ос­нов­ные ти­пы дан­ных яв­ля­ют­ся клас­са­ми Mar­s­hal­B­y­Va­lue - об­ра­ба­ты­ва­емых че­рез зна­че­ние, по­это­му им прис­ва­ива­ют­ся нор­маль­ные зна­че­ния. Класс string по преж­не­му яв­ля­ет­ся мас­си­вом, хоть и очень хит­рым, и об­ра­ба­ты­ва­ет­ся со­от­вет­с­т­вен­но.
    Инициализация бы­ва­ет двух ти­пов:
    для прос­тых ти­пов дан­ных это прос­тое прис­во­ение зна­че­ния:
    
    int i = 0;
    double d = 0.2;
    string s = "test string"
    Все это ва­ри­ан­ты дек­ла­ра­ции с од­нов­ре­мен­ной ини­ци­али­за­ци­ей для прос­тых ти­пов дан­ных.
    Второй ва­ри­ант - ини­ци­али­за­ция объ­ек­та клас­са:
    
    DateTime dt = new Da­te­Ti­me();
    MyClass mc = new MyClass();
    int[] in­tAr­ray = new int[20];
    Обратите вни­ма­ние - мас­си­вы рас­смат­ри­ва­ют­ся как клас­сы.
    Для спе­ци­алис­тов: мас­сив, как из­вес­т­но, об­ра­ба­ты­ва­ет­ся по ука­за­те­лю. А прос­тые ти­пы дан­ных то­же мо­гут быть ини­ци­али­зи­ро­ва­ны как клас­сы: int i = new int(); впол­не пра­виль­ная за­пись. Бо­лее то­го, за­пись "int i = 2;" для ком­пи­ля­то­ра рав­на "int i = new int(); i = 2;".
    Инициализировать пе­ре­мен­ную мож­но поч­ти в лю­бой мо­мент - глав­ное, до пер­во­го об­ра­ще­ния. Та­кая за­пись впол­не до­пус­ти­ма:
    
    public par­ti­al class Form1 : Form
    {
    public Da­te­Ti­me dt = new Da­te­Ti­me();
    И пос­лед­нее: ошиб­ка при ком­пи­ля­ции "Use of unas­sig­ned lo­cal va­ri­ab­le ..." зна­чит, что вы за­бы­ли ини­ци­али­зи­ро­вать пе­ре­мен­ную, дек­ла­ри­ро­ван­ную внут­ри фун­к­ции, а ошиб­ка "Null re­fe­ren­ce ex­cep­ti­on ..." при ра­бо­те прог­рам­мы оз­на­ча­ет, что вы за­бы­ли соз­дать объ­ект клас­са для пе­ре­мен­ной­, дек­ла­ри­ро­ван­ной на уров­не клас­са.
    
    Коллекции/списки и мас­си­вы
    Что та­кое мас­си­вы мы уже рас­смот­ре­ли, те­перь рас­смот­рим та­кую вещь как кол­лек­цию/спи­сок и срав­ним.
    Список - это лю­бой класс, ко­то­рый под­дер­жи­ва­ет ин­тер­фей­с IList, поз­во­ля­ющий соз­да­вать ди­на­ми­чес­кие мас­си­вы. Ди­на­ми­чес­кий мас­сив - на­бор объ­ек­тов од­но­го клас­са, с из­ме­ня­ющим­ся ко­ли­чес­т­вом и по­ряд­ком объ­ек­тов по хо­ду вы­пол­не­ния прог­рам­мы. Ба­зо­вый ин­тер­фей­с IList со­дер­жит фун­к­ции для до­бав­ле­ния но­во­го объ­ек­та к спис­ку, уби­ра­ния объ­ек­та из спис­ка, встав­ки объ­ек­та в спи­сок, по­ис­ка объ­ек­та и очис­т­ки спис­ка.
    Коллекция - лю­бой класс по­рож­ден­ный от клас­са Col­lec­ti­on­Ba­se, обес­пе­чи­ва­юще­го фун­к­ции под­дер­ж­ки спис­ка. Класс Col­lec­ti­on­Ba­se под­дер­жи­ва­ет ин­тер­фей­с IList.
    Есть мо­ди­фи­ка­ции этих двух клас­сов для соз­да­ния кол­лек­ций толь­ко-для-чте­ния, раз­но­ти­по­вых кол­лек­ций­, пар­ных мас­си­вов (хе­шей­) и пр.
    Преимущества спис­ков - ди­на­мич­ность вы­де­ля­емой па­мя­ти.
    Преимущества мас­си­вов - ско­рость.
    Как поль­зо­вать­ся - ес­ли очень не лень, или очень на­до - мо­же­те всег­да соз­дать свой класс. Та­кой ва­ри­ант мы рас­смот­рим поз­же, ког­да бу­дем го­во­рить о нас­ле­до­ва­нии. Про­ще все­го поль­зо­вать­ся клас­сом Ar­ray­List:
    
    ArrayList al = new Ar­ray­List();
    int i = 2;
    double d = 4.5;
    string s = "test";
    MyClass mc = new MyClass();
    al.Add(i);
    al.Add(d);
    al.Add(s);
    al.Add(mc);
    Таким пу­тем мы до­ба­ви­ли са­мые раз­ные эле­мен­ты в спи­сок и мо­жем до лю­бо­го из них дос­ту­чать­ся как до эле­мен­та мас­си­ва:
    
    string s2 = (string)al[2];
    s2 бу­дет рав­но "test". Есть толь­ко од­но не­удоб­с­т­во - Ar­ray­List всег­да воз­в­ра­ща­ет по­ме­щен­ный в не­го объ­ект как obj­ect, по­это­му для нор­маль­ной ра­бо­ты нуж­но пре­об­ра­зо­вы­вать воз­в­ра­ща­емое зна­че­ние в нуж­ный тип... что не всег­да прос­то. Имен­но это пре­об­ра­зо­ва­ние и со­вер­ша­ет сло­во string, сто­ящее в круг­лых скоб­ках пе­ред об­ра­ще­ни­ем al[2]. Под­роб­нее об этом - чуть ни­же.
    
    Типы дан­ных
    У каж­дой пе­ре­мен­ной есть свой тип дан­ных или прос­то тип, то же мож­но ска­зать о воз­в­ра­ща­емом лю­бой фун­к­ци­ей зна­че­нии (vo­id то­же тип, хо­тя и осо­бый­). В .NET все ти­пы дан­ных яв­ля­ют­ся до­чер­ни­ми от Obj­ect, у ко­то­ро­го есть все­го 4 фун­к­ции, за­то 2 из них нуж­ны очень час­то:
    Object.ToString() - воз­в­ра­ща­ет стро­ко­вое пред­с­тав­ле­ние об объ­ек­те (для чи­сел - стро­ку с чис­лом, для слож­ных объ­ек­тов - не­кую стро­ку, опи­сы­ва­ющую глав­ную ин­фор­ма­цию объ­ек­та; нап­ри­мер Fi­le­In­fo - класс ин­фор­ма­ции о фай­ле - в ме­то­де ToS­t­ring() воз­в­ра­ща­ет пол­ный путь фай­ла.)
    Object.GetType() - воз­в­ра­ща­ет объ­ект клас­са Type, опи­сы­ва­ющий тип дан­ных, ко­то­ро­му при­над­ле­жит ис­сле­ду­емый объ­ект.
    Для срав­не­ния ти­пов есть спе­ци­аль­ный опе­ра­тор 'is'. Ис­поль­зу­ет­ся так:
    
    if (var is int) { do so­met­hing }
    вместо var - имя пе­ре­мен­ной­, вмес­то int - тип дан­ных (имя клас­са) с ко­то­рым вы хо­ти­те срав­нить.
    Рассмотрим при­мер из пре­ды­ду­ще­го па­раг­ра­фа - у нас есть Ar­ray­List из 4 объ­ек­тов, 4 раз­ных ти­пов:
    
    for (int i = 0; i ‹ al.Length; i ++) {
    if (al[i] is int) { /*сде­лать что-то с це­лым чис­лом */ (int)al[i]}
    else if (al[i] is do­ub­le} { /*сде­лать что-то с дроб­ным чис­лом*/ (do­ub­le)al[i]}
    else if (al[i] is string} { /*сде­лать что-то со стро­кой­*/ (string)al[i]}
    else { Mes­sa­ge­Box.Show(al[i].ToS­t­ring()); } // по­ка­зать со­об­ще­ние со стро­ко­вым пред­с­тав­ле­ни­ем объ­ек­та
    }
    В этом при­ме­ре мы про­бе­га­ем цик­лом по спис­ку, смот­рим на тип оче­ред­ной пе­ре­мен­ной­, ес­ли это что-то прос­тое - пре­об­ра­зу­ем и что-то де­ла­ем, ес­ли нет - по­ка­зы­ва­ем в со­об­ще­нии стро­ко­вое пред­с­тав­ле­ние объ­ек­та.
    Теперь о пре­об­ра­зо­ва­нии. Пре­об­ра­зо­ва­ни­ем ти­пов дан­ных на­зы­ва­ют два раз­ных про­цес­са.
    Один - это кон­вер­та­ция (Con­vert) - нас­то­ящая сме­на ти­па. Нап­ри­мер, прев­ра­тить do­ub­le в int - т.е. от­б­ро­сить дроб­ную часть, или ок­руг­лить до бли­жай­ше­го це­ло­го. Или, еще нап­ри­мер, прев­ра­тить do­ub­le в string - т.е. соз­дать стро­ку, где за­пи­са­но чис­ло, хра­ни­мое в пе­ре­мен­ной.
    Второй - это сме­на ти­па (cast) - без за­ме­ны дан­ных. Пе­ре­мен­ная ти­па obj­ect мо­жет на са­мом де­ле хра­нить лю­бой объ­ект, и что­бы ком­пи­ля­тор по­нял, что от не­го хо­тят - на­до пре­об­ра­зо­вы­вать. Бы­ва­ют и бо­лее слож­ные ве­щи.
    Конвертация со­вер­ша­ет­ся всег­да че­рез фун­к­цию, ко­то­рая зна­ет, как имен­но на­до пре­об­ра­зо­вы­вать ти­пы. А все, что ка­са­ет­ся строк - на­до знать еще и нас­т­рой­ки куль­ту­ры для ко­то­рой ве­дет­ся пре­об­ра­зо­ва­ние. Есть спе­ци­аль­ный класс Con­vert, ко­то­рый ве­да­ет кон­вер­та­ци­ей. Им и ре­ко­мен­ду­ет­ся поль­зо­вать­ся.
    
    double d = 4.2;
    string s1 = d.ToS­t­ring();
    string s2 = Con­vert.ToS­t­ring(4.2);
    s1 и s2 - оди­на­ко­вы.
    Преобразование мо­жет быть: встро­ен­ным (impli­cit - не­яв­ным), над­с­т­ро­ен­ным (expli­cit - яв­ным) и еще од­ним :)... на­зо­вем его ус­лов­ным.
    Встроенное - оно ли­бо есть, ли­бо нет. Обес­пе­чи­ва­ет­ся соз­да­те­лем клас­са. Т.е. нап­ри­мер int в do­ub­le пре­об­ра­зу­ет­ся лег­ко, са­мим ком­пи­ля­то­ром, а вот на­обо­рот - толь­ко че­ло­ве­ком.
    Надстроенное - это то, что пи­шет­ся в скоб­ках пе­ред име­нем пе­ре­мен­ной. Нап­ри­мер, мож­но вот так вот сде­лать:
    
    CheckBox cb = new Chec­k­Box();
    object obj = cb;
    ((CheckBox)obj).Checked = true;
    Условное ра­бо­та­ет толь­ко с объ­ек­та­ми клас­сов и не ра­бо­та­ет с прос­ты­ми ти­па­ми дан­ных, кро­ме string. Выг­ля­дит оно как опе­ра­тор 'as' - 'как' (рас­смат­ри­вать как).
    
    string s = al[2] as string;
    Оператор го­во­рит ком­пи­ля­то­ру, что под­со­вы­ва­емую ему пе­ре­мен­ную на­до рас­смат­ри­вать "как" ука­зан­ный тип дан­ных.
    
Гла­ва 6. При­мер ко­да.
    
    Итак, для пер­вой час­ти, я ду­маю дос­та­точ­но. С тем, что уже бы­ло рас­смот­ре­но, мож­но на­чи­нать прог­рам­ми­ро­вать, а зна­ющие дру­гой язык дол­ж­ны бы­ли уже по­лу­чить пред­с­тав­ле­ние о C#. Ос­та­лись не­рас­смот­рен­ны­ми еще мно­гие ве­щи, вклю­чая не­ко­то­рые из ос­нов­ных, но в си­лу то­го, что они нес­коль­ко слож­нее - я рас­смот­рю их в сле­ду­ющих гла­вах.
    
    Пример ко­да:
    
    namespace tst_form2 //за­да­ем na­mes­pa­ce
    {
    public de­le­ga­te do­ub­le De­le­ga­te1(); // в нем дек­ла­ри­ру­ем де­ле­га­та
    public ab­s­t­ract class MyPa­ren­t­C­lass // соз­да­ем класс, аб­с­т­рак­т­ный
    : Obj­ect, tst_form3.IIn­ter­fa­ce1 // по­рож­ден­ный от клас­са Obj­ect с под­к­лю­чен­ным ин­тер­фей­сом tst_form3.IIn­ter­fa­ce1
    {
    private int int1; // внут­рен­нее по­ле, дос­туп­ное толь­ко внут­ри клас­са
    protected int Int1 { // свой­ст­во для это­го по­ля, дос­туп­ное нас­лед­ни­кам
    get { re­turn int1; } // воз­в­рат­ный код
    set { //уста­но­воч­ный код
    if (va­lue › 0) { // про­вер­ка на зна­че­ние
    int1 = va­lue; // ес­ли прош­ли - ус­та­но­вить зна­че­ние
    }
    else throw new Ar­gu­men­tEx­cep­ti­on("Va­lue must be › 0", "Int1"); // ес­ли нет - ки­нуть ошиб­ку
    }
    }
    public do­ub­le d1; // от­к­ры­тые по­ля
    public do­ub­le d2; //
    public MyPa­ren­t­C­lass() { //основ­ной кон­с­т­рук­тор клас­са
    int1 = 0; // ини­ци­али­зи­ру­ем пе­ре­мен­ную на­чаль­ным зна­че­ни­ем
    }
    public MyPa­ren­t­C­lass(do­ub­le inD1, do­ub­le inD2) // до­пол­ни­тель­ный кон­с­т­рук­тор клас­са
    : this() { // ко­то­рый сна­ча­ла за­пус­ка­ет ос­нов­ной кон­с­т­рук­тор и толь­ко по­том ис­пол­ня­ет­ся
    d1 = inD1; // ини­ци­али­зи­ру­ем пе­ре­мен­ные вхо­дя­щи­ми ар­гу­мен­та­ми
    d2 = inD2; // ес­ли ар­гу­мен­тов нет - пе­ре­мен­ные ини­ци­али­зи­ру­ют­ся са­ми, стан­дар­т­ны­ми зна­че­ни­ями
    }
    protected ab­s­t­ract do­ub­le Get­Sum(); // шаб­лон фун­к­ции для ини­ци­али­за­ции в до­чер­них клас­сах
    #region IIn­ter­fa­ce1 Mem­bers // фун­к­ции ин­тер­фей­са
    public ab­s­t­ract do­ub­le Get­Sub­s­t­ract(); // фун­к­ция ин­тер­фей­са - то­же толь­ко шаб­лон
    #endregion
    }
    namespace tst_form3 // объ­яв­ля­ем еще na­mes­pa­ce внут­ри tst_form2
    {
    public class MyChil­d­C­lass //соз­да­ем класс
    : tst_form2.MyPa­ren­t­C­lass //по­рож­ден­ный от tst_form2.MyPa­ren­t­C­lass
    {
    private De­le­ga­te1 get­Sum­Del; // дек­ла­ри­ру­ем объ­ект де­ле­га­та
    internal MyChil­d­C­lass() //основ­ной кон­с­т­рук­тор
    : ba­se() { // за­пус­ка­ющий ро­ди­тель­с­кий и толь­ко по­том ис­пол­ня­ющий­ся
    getSumDel = new De­le­ga­te1(Get­Sum); // ини­ци­али­зи­ру­ем объ­ект де­ле­га­та - на фун­к­цию Get­Sum
    }
    internal MyChil­d­C­lass(do­ub­le inD1, do­ub­le inD2) //до­пол­ни­тель­ный кон­с­т­рук­тор
    : ba­se(inD1, inD2) { // за­пус­ка­ющий до­пол­ни­тель­ный ро­ди­тель­с­кий кон­с­т­рук­тор
    getSumDel = new De­le­ga­te1(Get­Sum); // ини­ци­али­зи­ру­ющий объ­ект де­ле­га­та
    }
    #region in­he­ri­ted mem­bers // унас­ле­до­ван­ные фун­к­ции
    protected over­ri­de do­ub­le Get­Sum() { // оп­ре­де­ля­ем ро­ди­тель­с­кую аб­с­т­рак­т­ную фун­к­цию
    return d1 + d2; //воз­в­ра­ща­ем сум­му
    }
    public over­ri­de do­ub­le Get­Sub­s­t­ract() { // оп­ре­де­ля­ем ро­ди­тель­с­кую аб­с­т­рук­т­ную фун­к­цию
    return d1 - d2; // воз­в­ра­ща­ем раз­ность
    }
    #endregion
    internal vo­id Chan­ge­Int1(int inInt) { // соз­да­ем фун­к­цию для сме­ны зна­че­ния
    Int1 = inInt;
    }
    public do­ub­le TryTo­Get­Sum() { //фун­к­ция по­лу­че­ния сум­мы
    return Sta­tic­Func.Get­Sum­F­rom­C­hil­d­C­lass(get­Sum­Del); // вы­зы­ва­ем ста­тич­ную фун­к­цию дру­го­го клас­са с де­ле­га­том для Get­Sum
    }
    }
    public in­ter­fa­ce IIn­ter­fa­ce1 //интер­фей­с
    {
    double Get­Sub­s­t­ract(); // фун­к­ции ин­тер­фей­сов объ­яв­ля­ют­ся без мо­ди­фи­ка­то­ров
    }
    }
    internal class Sta­tic­Func //класс для на­шей ста­тич­ной фун­к­ции
    {
    public sta­tic do­ub­le Get­Sum­F­rom­C­hil­d­C­lass(De­le­ga­te1 del) {
    return del.Invo­ke(); // воз­в­ра­ща­ем ре­зуль­тат вы­зо­ва фун­к­ции, при­пи­сан­ной к де­ле­га­ту
    }
    public enum Sup­por­ted­Lan­gu­ages //прос­той Enum
    {
    Русский = 1,
    English = 10,
    German = 11
    }
    public sta­tic obj­ect[] obj­Ar­ray = {1, 2.4, "string", Sup­por­ted­Lan­gu­ages.Рус­ский }; // ста­тич­ный мас­сив
    }
    }//закрываем na­mes­pa­ce tst_form2
    Для тес­т­ро­ва­ния - на­пи­шем еще та­кую фун­к­цию в лю­бом дру­гом na­mes­pa­ce... хоть в ос­нов­ном, соз­дан­ном ав­то­ма­ти­чес­ки при соз­да­нии про­ек­та:
    
    private vo­id but­ton1_Click(obj­ect sen­der, Even­tArgs e) { //при на­жа­тии на кноп­ку
    tst_form2.tst_form3.MyChildClass mcc = new tst_form2.tst_form3.MyChil­d­C­lass(1.2, 2.4); // соз­да­ем объ­ект на­ше­го клас­са с вве­ден­ны­ми зна­че­ни­ями
    double tmp_do­ub­le = mcc.TryTo­Get­Sum(); //вер­нет 3.6
    tmp_double = mcc.Get­Sub­s­t­ract(); //вер­нет -1.2
    /*mcc.ChangeInt1(-2); //про­изой­дет ошиб­ка - не­вер­ный па­ра­метр*/
    mcc.ChangeInt1(2); //int1 ста­нет ра­вен 2
    int i1 = (int)tst_form2.Sta­tic­Func.Sup­por­ted­Lan­gu­ages.English; // i1 = 10
    string s = tst_form2.Sta­tic­Func.Sup­por­ted­Lan­gu­ages.English.ToS­t­ring();//s="English"
    s = "";
    for (int i = 0; i ‹ tst_form2.Sta­tic­Func.obj­Ar­ray.Length; i++) {
    s += tst_form2.Sta­tic­Func.obj­Ar­ray[i].ToS­t­ring() + "\t";
    if (tst_form2.Sta­tic­Func.obj­Ar­ray[i] is int) s += "\n";
    else {
    switch (tst_form2.Sta­tic­Func.obj­Ar­ray[i].Get­T­y­pe().ToS­t­ring()) {
    case "System.Do­ub­le":
    s += Math.Ro­und((do­ub­le)tst_form2.Sta­tic­Func.obj­Ar­ray[i]).ToS­t­ring() + "\n";
    break;
    case "tst_form2.Sta­tic­Func+Sup­por­ted­Lan­gu­ages":
    s += ((int)(tst_form2.Sta­tic­Func.Sup­por­ted­Lan­gu­ages)tst_form2.Sta­tic­Func.obj­Ar­ray[i]).ToS­t­ring();
    break;
    default:
    s += "\n";
    break;
    }
    }
    } // цикл за­вер­ша­ет­ся s = "1
    //2.4 2
    //string
    //Русский 1"
    }
    
    Немного до­пол­не­ний и ком­мен­та­ри­ев:
    Если у фун­к­ции есть мо­ди­фи­ка­тор over­ri­de - ее мож­но пе­ре­оп­ре­де­лять во всех до­чер­них клас­сах, лю­бо­го по­ко­ле­ния.
    Если вы хо­ти­те сра­зу оп­ре­де­лить фун­к­цию, но дать ей воз­мож­ность быть пе­ре­оп­ре­де­лен­ной в нас­лед­ни­ках - прис­вой­те мо­ди­фи­ка­тор vir­tu­al.
    Последовательное вы­пол­не­ние ус­ло­вий в switch бло­ке воз­мож­но толь­ко ес­ли ca­se ус­ло­вия оп­ре­де­ле­ны чис­ла­ми.
    По по­во­ду плю­си­ка в име­ни ти­па Enum - Enum, по ло­ги­ке, на­до объ­яв­лять в na­mes­pa­ce, на­рав­не с клас­са­ми. Од­на­ко, мож­но их за­пи­хать и внутрь клас­са. Ес­ли их объ­явить в na­mes­pa­ce - имя ти­па бу­дет пос­т­ро­ено как обыч­но, а ес­ли внут­ри клас­са - то че­рез плю­сик.
    
Часть 2.
    Чуть бо­лее слож­ные ве­щи в язы­ке C# и плат­фор­ме .NET.
Глава 1. Модификаторы аргументов. Регулярные выражения.
    
    Рассмотрим мо­ди­фи­ка­то­ры ар­гу­мен­тов фун­к­ций.
    ref - пе­ре­да­ет в ка­чес­т­ве ар­гу­мен­та ссыл­ку на объ­ект. Та­ким об­ра­зом вы по­лу­ча­ете воз­мож­ность из­ме­нять объ­ект, не соз­да­вая но­вый­, и не воз­в­ра­щая его, как ре­зуль­тат фун­к­ции.
    Пример:
    
    private vo­id Func1(ref string str) {
    str = "но­вая стро­ка";
    }
    private vo­id Ma­in­Func() {
    string str = "стро­ка";
    Func1(ref str); // str = "но­вая стро­ка"
    }
    out - пе­ре­да­ет в ка­чес­т­ве ар­гу­мен­та ссыл­ку на не­ини­ци­али­зи­ро­ван­ный объ­ект. Та­ким об­ра­зом вы по­лу­ча­ете воз­мож­ность воз­в­ра­щать мно­жес­т­вен­ные ре­зуль­та­ты ра­бо­ты фун­к­ции.
    Пример:
    
    private vo­id Func1(out string re­sult1, out int re­sult2, out obj­ect re­sult3) {
    result1 = 10;
    result2 = "ре­зуль­тат";
    result3 = new obj­ect();
    }
    private vo­id Ma­in­Func() {
    int r1;
    string r2;
    object r3;
    Func1(out r1, out r2, out r3);
    }
    Разница меж­ду ref и out толь­ко в том, что out не тре­бу­ет ини­ци­али­за­ции ар­гу­мен­та до вы­зо­ва фун­к­ции, а тре­бу­ет ее внут­ри фун­к­ции. ref - на­обо­рот, тре­бу­ет пе­ре­да­чи ини­ци­али­зи­ро­ван­но­го объ­ек­та.
    
    param - очень хит­рая вещь, поз­во­ля­ет де­лать фун­к­ции с пе­ре­мен­ным ко­ли­чес­т­вом ар­гу­мен­тов. Тре­бо­ва­ния прос­ты - pa­ram ар­гу­мент дол­жен быть пос­лед­ним в спис­ке ар­гу­мен­тов, ар­гу­мен­ты дол­ж­ны быть од­но­го ти­па, ар­гу­мен­ты за­да­ют­ся как мас­сив.
    Например так:
    
    private vo­id Func1(byte b1, pa­ram int[] da­ta) {}
    Если вам не­об­хо­ди­мо, что­бы ар­гу­мен­ты бы­ли раз­но­тип­ны - счи­тай­те их ти­пом obj­ect. Еще один мо­мент - вмес­то спис­ка ар­гу­мен­тов в стро­ке, мож­но пе­ре­да­вать мас­сив... но тут на­до быть ос­то­рож­ным:
    
    private vo­id Func1(byte b1, pa­ram obj­ect[] da­ta) { }
    private vo­id Ma­in­Func() {
    Func1(1, 1, "str", new obj­ect()); // все нор­маль­но. da­ta - мас­сив ти­па obj­ect из трех чле­нов.
    Func1(2, new obj­ect[] {1, "str", new obj­ect()}); // все нор­маль­но, da­ta - как в пре­ды­ду­щем ва­ри­ан­те.
    Func1(3, 1, new obj­ect[]{"str", new obj­ect()}); // da­ta - мас­сив ти­па obj­ect из двух чле­нов, вто­рой - мас­сив ти­па obj­ect из 2х чле­нов.
    }
    
 Регулярные вы­ра­же­ния
    В об­щем ви­де ре­гу­ляр­ные вы­ра­же­ния ( = re­gu­lar ex­p­res­si­ons = Re­gEx) - это шаб­лон тек­с­то­вой стро­ки. Поч­ти все за­да­чи ка­са­ющи­еся по­ис­ка и за­ме­ны в стро­ках пред­поч­ти­тель­но ре­шать че­рез них. За­да­чи по­ис­ка и за­ме­ны вклю­ча­ют в се­бя по­иск тек­с­та, ре­дак­ти­ро­ва­ние тек­с­та, пре­об­ра­зо­ва­ние тек­с­та (фор­ма­ти­ро­ва­ние), вы­би­ра­ние по­лез­ной ин­фор­ма­ции из тек­с­та для пос­ле­ду­юще­го ис­поль­зо­ва­ния и пр. Поль­зо­вать­ся ре­гу­ляр­ны­ми вы­ра­же­ни­ями име­ет смысл ког­да объ­ем тек­с­та дос­та­точ­но ве­лик. Для ра­бо­ты с ре­гу­ляр­ны­ми вы­ра­же­ни­ями су­щес­т­ву­ет класс Re­gex (System.Text.Re­gu­la­rEx­p­res­si­ons.Re­gex).
    Использоваться этот класс мо­жет тре­мя пу­тя­ми:
    1. поль­зо­вать­ся ста­ти­чес­ки­ми ме­то­да­ми клас­са. Пред­поч­ти­тель­но, ес­ли прог­рам­ме это ред­ко нуж­но, и не при каж­дом за­пус­ке, а так­же, ес­ли са­мо вы­ра­же­ние ис­поль­зу­ет­ся толь­ко один раз под­ряд.
    2. Соз­да­вать объ­ект клас­са re­gex в не­ком­пи­ли­ру­емом ви­де (флаг Com­pi­led не ус­та­нов­лен) и поль­зо­вать­ся его фун­к­ци­ями. Пред­поч­ти­тель­но ес­ли прог­рам­ме это ред­ко нуж­но, и не при каж­дом за­пус­ке, но вы­ра­же­ние ис­поль­зу­ет­ся нес­коль­ко раз под­ряд.
    3. Соз­да­вать объ­ект клас­са re­gex в ком­пи­ли­ру­емом ви­де (флаг Com­pi­led ус­та­нов­лен) и поль­зо­вать­ся его фун­к­ци­ями. Пред­поч­ти­тель­но ес­ли прог­рам­ма час­то поль­зу­ет­ся вы­ра­же­ни­ем.
    
    Регулярное вы­ра­же­ние сос­то­ит из шаб­ло­на и оп­ций. С оп­ци­ями ра­зоб­рать­ся до­воль­но прос­то, а вот шаб­ло­ны - это от­дель­ный язык. По по­во­ду за­да­ния шаб­ло­нов мо­гу толь­ко по­со­ве­то­вать поль­зо­вать­ся раз­ны­ми вспо­мо­га­тель­ны­ми прог­рам­ма­ми ти­па Ex­p­res­so и пр.
    Возьмем для при­ме­ра прос­той шаб­лон:
    
    (?‹Protocol›\w+):\/\/(?‹Domain›[\w\.]+)\/?\S*
    Этот шаб­лон вы­дер­ги­ва­ет из пред­ло­жен­но­го тек­с­та все URL, и сох­ра­ня­ет их с вы­де­ле­ни­ем про­то­ко­ла и до­ме­на.
    Код для ис­поль­зо­ва­ния:
    
    void Re­gex­Match() {
    string inStr = "blah-blah-blah so­met­hing he­re http://www.do­ma­in.com/index.cgi and so­me com­ments he­re and fi­le he­re ftp://ftp.do­ma­in123.info/re­po­si­tory/";
    StringBuilder sb = new Strin­g­Bu­il­der();
    Regex r = new Re­gex(@"(?‹Pro­to­col›\w+):\/\/(?‹Do­ma­in›[\w\.]+)\/?\S*", Re­ge­xOp­ti­ons.Igno­re­Ca­se | RegexOptions.Singleline);
    for (Match m = r.Mat­ch(inStr); m.Suc­cess; m = m.Nex­t­Match()) {
    sb.AppendFormat("Найдено URL: {0}, про­то­кол: {1}, до­мен: {2}\r\n", m.Va­lue, m.Gro­ups["Pro­to­col"].Va­lue, m.Gro­ups["Do­ma­in"].Va­lue);
    }
    textBox1.Text = sb.ToS­t­ring();
    }
    Результат бу­дет:
    
    Найдено URL: http://www.do­ma­in.com/index.cgi, про­то­кол: http, до­мен: www.do­ma­in.com
    Найдено URL: ftp://ftp.do­ma­in123.info/re­po­si­tory/, про­то­кол: ftp, до­мен: ftp.do­ma­in123.info
    
    Примеров мо­жет быть мно­жес­т­во, луч­ше пос­мот­ри­те шаб­ло­ны в дей­ст­вии... а в Ex­p­res­so есть сис­те­ма ин­те­рак­тив­но­го сос­тав­ле­ния шаб­ло­на... очень по­лез­но для обу­че­ния, да и при­ме­ров у них мно­го.
    
Гла­ва 2. Ге­не­ало­гия клас­сов.
    
    Итак, по­го­во­рим ког­да ко­го и как нуж­но по­рож­дать.
    Один из пос­ту­ла­тов сов­ре­мен­но­го вы­со­ко­уров­не­во­го прог­рам­ми­ро­ва­ния, не всег­да вер­ных, впро­чем, гла­сит, что "Чем мень­ше оди­на­ко­вых бло­ков ко­да в прог­рам­ме - тем луч­ше". Пер­вое след­с­т­вие из это­го пос­ту­ла­та - блок ко­да, ко­то­рый ис­поль­зу­ет­ся боль­ше, чем в од­ном мес­те прог­рам­мы дол­жен быть вы­не­сен в от­дель­ную фун­к­цию. Впро­чем, это к те­ме не от­но­сит­ся. Вто­рое след­с­т­вие - на­бор пе­ре­мен­ных/фун­к­ций­, ко­то­рые ис­поль­зу­ют­ся боль­ше чем в од­ном клас­се дол­ж­ны быть вы­де­ле­ны в от­дель­ный ро­ди­тель­с­кий класс. В об­щем-то, этим пра­ви­лом мож­но ру­ко­вод­с­т­во­вать­ся при оп­ре­де­ле­нии "ну­жен ли вам не­кий ро­ди­тель для ва­ших клас­сов?", ес­ли у вас их мно­го.
    
    Рассмотрим мо­дель­ную си­ту­ацию с мно­жес­т­вом клас­сов, и оп­ре­де­лим ка­кие им нуж­ны род­с­т­вен­ные свя­зи.
    
    Довольно клас­си­чес­кий при­мер - эле­мен­ты уп­рав­ле­ния. Рас­смот­рим 4 эле­мен­та уп­рав­ле­ния: кноп­ка, тек­с­то­вое по­ле, па­нель и груп­па. У всех этих эле­мен­тов дол­ж­ны быть свой­ст­ва: по­ло­же­ние, раз­мер, со­бы­тия на­жа­тия, фо­ку­са и еще мож­но мно­го вся­ких на­пи­сать для удоб­с­т­ва прог­рам­мис­тов. Ста­ло быть один пре­док, об­щий для всех эле­мен­тов уп­рав­ле­ния на­шел­ся. Ду­ма­ем даль­ше - па­нель и груп­па дол­ж­ны иметь под­чи­нен­ные эле­мен­ты уп­рав­ле­ния, а так­же уметь прок­ру­чи­вать соб­с­т­вен­ную внут­рен­нюю об­ласть. Ста­ло быть у этих дво­их дол­жен быть еще об­щий пре­док. Итак струк­ту­ра по­лу­чить­ся при­мер­но та­кой­:
    
    Ну вот вам об­щий прин­цип вы­де­ле­ния об­щих пред­ков.
    
    Рассмотрим воп­ро­сы нас­лед­с­т­ва, ог­ра­ни­че­ния на нас­лед­с­т­во, рас­т­ран­жи­ри­ва­ния нас­лед­с­т­ва и сте­ри­ли­за­ции.
    
    Многое из то­го, о чем сей­час пой­дет речь уже рас­ска­зы­ва­лось в час­ти 1 гла­ве 1 в раз­де­ле о дек­ла­ра­ци­ях.
    Главное пра­ви­ло нас­ле­до­ва­ния - каж­дый по­то­мок нас­ле­ду­ет все. Т.е. все чле­ны клас­са, от­к­ры­тые для нас­ле­до­ва­ния, пе­ре­да­ют­ся каж­до­му по­том­ку в лю­бом ко­ле­не. Един­с­т­вен­ное, что мо­жет по­ме­шать - пе­ре­оп­ре­де­ле­ние чле­на клас­са.
    
 По по­во­ду "как жить по­том­кам":
    Итак, мо­ди­фи­ка­тор ab­s­t­ract у клас­са го­во­рит о том, что сам класс ни­че­го сде­лать не мо­жет, толь­ко нас­лед­с­т­во ос­та­лось. Бу­дем счи­тать его без­в­ре­мен­но по­чив­шим род­с­т­вен­ни­ком... тем са­мым дя­дей "са­мых чес­т­ных пра­вил", ко­то­рый од­на­ко весь­ма чет­ко опи­сал как имен­но дол­ж­ны жить его по­том­ки. Тот же мо­ди­фи­ка­тор у чле­на клас­са го­во­рит о том, что класс счи­та­ет этот член обя­за­тель­ным у сво­их по­том­ков, но сам тол­ком не зна­ет что это та­кое. Эда­кий стар­ший род­с­т­вен­ник, зна­ющий как дол­ж­ны жить его по­том­ки, но сам жи­ву­щий по-дру­го­му.
    Модификатор vir­tu­al у чле­на клас­са го­во­рит о том, что класс зна­ет что де­лать, од­на­ко до­пус­ка­ет иное тол­ко­ва­ние для по­том­ков. Хо­ро­ший род­с­т­вен­ник с ши­ро­ки­ми взгля­да­ми :).
    
 Теперь ка­са­тель­но дос­ту­па к нас­лед­с­т­ву:
    модификатор pri­va­te обес­пе­чи­ва­ет неп­ри­кос­но­вен­ность нас­лед­с­т­ва. Дос­туп есть толь­ко из дру­гих унас­ле­до­ван­ных фун­к­ций. Нап­ри­мер, у ро­ди­те­ля есть от­к­ры­тая фун­к­ция pub­lic1, и зак­ры­тая фун­к­ция pri­va­te1. По­то­мок мо­жет об­ра­тить­ся толь­ко к pub­lic1, но ес­ли внут­ри ко­да pub­lic1 есть об­ра­ще­ние к pri­va­te1, то все бу­дет ра­бо­тать.
    модификатор pro­tec­ted, в воп­ро­сах нас­лед­с­т­ва, эк­ви­ва­лен­тен pub­lic - нас­лед­с­т­во от­к­ры­то для дос­ту­па. "Поль­зуй­тесь, род­с­т­вен­нич­ки до­ро­гие!"
    
 Теперь о соб­с­т­вен­ном мне­нии по­том­ков по по­во­ду нас­лед­с­т­ва.
    Вобщем-то, ник­то не ме­ша­ет по­том­кам вы­би­рать из нас­лед­с­т­ва толь­ко пон­ра­вив­ши­еся час­ти. Да и по­сы­лать стар­ших с их со­ве­та­ми "как на­до жить" то­же. Лю­бая фун­к­ция, объ­яв­лен­ная vir­tu­al мо­жет быть пе­ре­оп­ре­де­ле­на мо­ди­фи­ка­то­ром over­ri­de. Ес­ли очень на­до пе­ре­оп­ре­де­лить фун­к­цию не от­ме­чен­ную vir­tu­al - мож­но ис­поль­зо­вать мо­ди­фи­ка­тор new. Прав­да с этим на­до быть ос­то­рож­ным - ес­ли уж вы нас­ле­ду­ете от ко­го-то, к вам бу­дут со­от­вет­с­т­вен­но от­но­сит­ся и мо­жет быть кон­ф­ликт с дру­ги­ми клас­са­ми, жду­щи­ми од­ной под­пи­си фун­к­ции, а на­па­ры­ва­ющи­ми­ся на дру­гую. Да и от род­ни не уй­дешь - до ори­ги­наль­но­го ва­ри­ан­та фун­к­ции все рав­но всег­да мож­но дос­ту­чать­ся, ес­ли знать что он есть. Из­нут­ри по­том­ка дос­та­точ­но ис­поль­зо­вать клю­че­вое сло­во ba­se, а сна­ру­жи - пре­об­ра­зо­вать тип объ­ек­та в тип пред­ка. Пред­по­ло­жим, фун­к­ция Func1 пе­ре­оп­ре­де­ле­на в клас­се B, по­том­ке клас­са А с ис­поль­зо­ва­ние сло­ва new:
    
    B b1 = new B();
    b1.Func1(); // но­вый ва­ри­ант фун­к­ции.
    ((A)b1).Func1(); // ста­рый ва­ри­ант фун­к­ции
 И са­мое страш­ное - сте­ри­ли­за­ция.
    Очень прос­то де­ла­ет­ся, как и все ужа­сы в на­шем ми­ре. Мо­ди­фи­ка­тор клас­са se­aled - сте­ри­ли­зу­ет его, и у не­го уже ни­ког­да не бу­дет де­тей :(. Тот же мо­ди­фи­ка­тор, при­ме­нен­ный к vir­tu­al фун­к­ции от­ме­ня­ет ее вир­ту­аль­ность.
    
 И на­пос­ле­док - крес­т­ные (отцы/ма­те­ри/дя­ди/те­ти/феи и пр. фоль­к­лор­ные эле­мен­ты)
    В ро­ли крес­т­ных выс­ту­па­ют ин­тер­фей­сы. Их у каж­до­го клас­са мо­жет быть сколь­ко угод­но, и для то­го, что­бы иметь крес­т­ных не обя­за­тель­но иметь ро­ди­те­лей. Ин­тер­фей­сы все очень стро­гие - они точ­но зна­ют что дол­жен уметь де­лать их крес­т­ник, и не от­с­та­ют, по­ка он все­му это­му не на­учить­ся. При­чем все, че­му учит ин­тер­фей­с обя­за­тель­но ос­та­ет­ся в дос­туп­ном нас­лед­с­т­ве (pub­lic). Ну а воз­мож­ность по­том­ков ре­шать са­мим за­ви­сит толь­ко от пред­ка, но ни­как не от крес­т­но­го. Это в клас­се-пред­ке оп­ре­де­ля­ет­ся бу­дет ли фун­к­ция с мо­ди­фи­ка­то­ром vir­tu­al или нет, да и что имен­но бу­дет де­лать фун­к­ция то­же оп­ре­де­ля­ет­ся там.
    Часто бы­ва­ет так, что раз­ные ин­тер­фей­сы под од­ним и тем же име­нем ра­зу­ме­ют раз­ные ве­щи... тог­да клас­су ни­че­го не ос­та­ет­ся, как вы­учить оба тол­ко­ва­ния, да еще за­пом­нить от ко­го ка­кое он по­лу­чил. Выг­ля­дит это при­мер­но так:
    каждое по­ле ин­тер­фей­са мо­жет быть им­п­ле­мен­ти­ро­ва­но в клас­се внут­рен­ним и внеш­ним об­ра­зом
    
    class Class1 : IIn­ter­fa­ce1 {
    public vo­id in­ter­fa­ce­Func1() {}; // внут­рен­няя им­п­ле­мен­та­ция
    public vo­id IIn­ter­fa­ce1.inter­fa­ce­Func2() {}; // внеш­няя им­п­ле­мен­та­ция
    }
    Имплементирование внеш­ним об­ра­зом яв­но ука­зы­ва­ет на ис­точ­ник тол­ко­ва­ния ка­ко­го-ли­бо по­ля. Та­кой под­ход поз­во­ля­ет из­бе­жать оши­бок при ком­пи­ля­ции и при пос­ле­ду­ющих об­ра­ще­ни­ях. Кста­ти, за­пом­ни­те, ес­ли вам нуж­на от объ­ек­та фун­к­ция ка­ко­го-то из его ин­тер­фей­сов - всег­да об­ра­щай­тесь к ней пред­ва­ри­тель­но пре­об­ра­зо­вав тип объ­ек­та в тип ин­тер­фей­са. Нап­ри­мер: объ­ект ob1 им­п­ле­мен­ти­ру­ет нес­коль­ко ин­тер­фей­сов, в том чис­ле IIn­ter­fa­ce1, с нуж­ной нам фун­к­ци­ей in­t­Func1:
    
    ((IInterface1)ob1).intFunc1();
    
    Этим вы да­ете по­нять, что вам нуж­на фун­к­ция in­t­Func1 имен­но в трак­тов­ке IIn­ter­fa­ce1, а не в чьей­-ни­будь дру­гой. А то фун­к­цию On­Pa­int кто толь­ко не им­п­ле­мен­ти­ру­ет...
    
Глава 3. Исключения.
    
    Исключения (Excep­ti­ons) - это то, что слу­ча­ет­ся, ког­да что-то неп­ра­виль­но. Нап­ри­мер, вы пы­та­етесь от­к­рыть файл, ко­то­ро­го нет - сис­тем­ная биб­ли­оте­ка ки­нет ис­к­лю­че­ние "файл не най­ден".
    Исключения на­до уметь ки­дать (throw) и ло­вить (catch).
    Лично я знаю 3 ме­то­ди­ки ра­бо­ты с ис­к­лю­че­ни­ями, каж­дая из ко­то­рых на­пи­са­на прог­рам­мис­том с боль­шим опы­том и мно­го­мет­ро­вым спис­ком ре­га­лий­, при­чем каж­дая из них ссы­ла­ет­ся на ка­ко­го-ни­будь ве­ли­ко­го гу­ру (одно­го из соз­да­те­лей .net, нап­ри­мер). На­до за­ме­тить, что эти три ме­то­ди­ки во мно­гом про­ти­во­ре­чат друг дру­гу. Вы­вод - ни­ка­кой "пра­виль­ной­" или "луч­шей­" ме­то­ди­ки ра­бо­ты с ис­к­лю­че­ни­ями нет. Есть толь­ко не­ко­то­рые пра­ви­ла, и чье-то мне­ние :).
    Рассмотрим ос­но­вы ра­бо­ты с ис­к­лю­че­ни­ями и те пра­ви­ла, ко­то­рые оди­на­ко­вы во всех трех ме­то­ди­ках.
    
    Как ки­дать ис­к­лю­че­ния
    Для ки­да­ния су­щес­т­ву­ет клю­че­вое сло­во throw.
    
    throw new Ex­cep­ti­on("Про­изош­ла ошиб­ка...");
    
Когда их кидать? - Когда что-то пошло не так. Впрочем, обычно это относится к библиотекам, которые вы пишете для других. Но в жизни всякое бывает и мой последний проект, например, состоит из 4 библиотек, которыми никто, кроме меня пользоваться не будет, однако они все исправно сообщают об ошибках исключениями.
    Например, вы пи­ше­те фун­к­цию рас­че­та че­го-ни­будь по двум дроб­ным вхо­дя­щим ар­гу­мен­там, но вам нуж­но что­бы ар­гу­мен­ты бы­ли боль­ше 1, ина­че ни­че­го не пос­чи­та­ет­ся... Вы мо­же­те про­ве­рять ар­гу­мен­ты и ес­ли они мень­ше - воз­в­ра­щать do­ub­le.NaN. Так ус­т­ро­ены ма­те­ма­ти­чес­кие фун­к­ции клас­са Math. А мо­же­те сде­лать так:
    
    private do­ub­le func1(do­ub­le arg1, do­ub­le arg2) {
    if (arg1 ‹= 1) throw new Ar­gu­men­tEx­cep­ti­on("Аргу­мент дол­жен быть боль­ше 1", "arg1");
    if (arg2 ‹= 1) throw new Ar­gu­men­tEx­cep­ti­on("Аргу­мент дол­жен быть боль­ше 1", "arg2");
    // рас­чет
    }
    
Такой код позволит указать программисту на ошибки. Собственно примерно так и надо писать библиотеки, которыми будут пользоваться другие.
    Еще си­ту­ация ког­да на­до ки­дать ис­к­лю­че­ния - ког­да вы пе­рех­ва­ты­ва­ете ис­к­лю­че­ние в сво­ей фун­к­ции, но вам на­до до­ба­вить к не­му ка­кую-то ин­фор­ма­цию.... нап­ри­мер, так:
    
    try {
    using (Fi­leS­t­re­am fs = Fi­le.Open­Re­ad(@"C:\\temp.txt")) {
    }
    }
    catch (Fi­le­Not­Fo­un­dEx­cep­ti­on ex) {
    throw new In­va­li­dO­pe­ra­ti­onEx­cep­ti­on("Ошиб­ка от­к­ры­тия фай­ла в мо­ей фун­к­ции...", ex);
    }
    
Такой код позволяет поймать и обработать исключение на уровне функции (например, закрыть потоки или еще чего сделать) и передать пойманное исключение дальше, с довеском в виде собственной информации.
    
    Как ло­вить ис­к­лю­че­ния
    Для от­ло­ва ис­к­лю­че­ний ис­поль­зу­ет­ся кон­с­т­рук­ция try {} catch {} fi­nal­ly {}.
    Блок try (поп­ро­бо­вать) со­дер­жит код, ко­то­рый мо­жет выз­вать ошиб­ку.
    Блок catch (пой­мать) со­дер­жит код об­ра­бот­ки ошиб­ки. Та­ких бло­ков мо­жет быть нес­коль­ко - на каж­дый тип ошиб­ки, ко­то­рые вы жде­те.
    Блок fi­nal­ly (на­пос­ле­док) не­обя­за­те­лен, в не­го по­ме­ща­ет­ся код, ко­то­рый вы­пол­ня­ет­ся в лю­бом слу­чае - за­кон­чил­ся ли блок try нор­маль­но, или ки­нул ис­к­лю­че­ние. Осо­бая фи­ча бло­ка fi­nal­ly - он вы­пол­ня­ет­ся да­же ес­ли фун­к­ция уже вер­ну­ла зна­че­ние.
    Пример:
    
    try {
    // опас­ный код
    }
    catch (Argu­men­tEx­cep­ti­on aex) {
    MessageBox.Show(aex.Message, "Error oc­cu­red");
    return 1;
    }
    catch (Excep­ti­on ex) {
    throw new In­va­li­dO­pe­ra­ti­onEx­cep­ti­on("Cus­tom mes­sa­ge...", ex);
    }
    finally {
    // зак­рыть по­то­ки и пр.
    }
    К сло­ву:
    Вместо бло­ка fi­nal­ly мож­но ис­поль­зо­вать кон­с­т­рук­цию using () {}. Эта кон­с­т­рук­ция поз­во­ля­ет убе­дить­ся, что все ре­сур­сы за­ня­тые не­ким объ­ек­том бу­дут пе­ре­ки­ну­ты в му­сор или уда­ле­ны, в за­ви­си­мос­ти от ти­па, по за­вер­ше­нии кус­ка ко­да, вне за­ви­си­мос­ти от ре­зуль­та­тов это­го са­мо­го ко­да.
    пример:
    
    using (MyClass obj­ect1 = new MyClass()) {
    // код, ис­поль­зу­ющий obj­ect1
    }
    Даже ес­ли код ки­нет ис­к­лю­че­ние - obj­ect1 бу­дет уда­лен. Для бло­ка using не обя­за­тель­на ини­ци­али­за­ция а бло­ке, мож­но и так:
    
    MyClass ob1 = new MyClass();
    using (ob1) {}
    Конструкция using мо­жет выс­ту­пать за­ме­ни­те­лем бло­ка fi­nal­ly толь­ко ес­ли вам в этом бло­ке на­до бы­ло уда­лить один объ­ект. Впро­чем, это очень час­то слу­ча­ет­ся. С дру­гой сто­ро­ны, она весь­ма по­лез­на, ког­да код, ис­поль­зу­ющий объ­ект име­ет нес­коль­ко то­чек воз­в­ра­ще­ния. Что­бы точ­но не за­быть снес­ти объ­ект - про­ще зак­лю­чить код в блок using. При­мер:
    
    MyClass ob1 = new MyClass();
    // что-то сде­лать с ob1
    if (ob1.pro­perty1 == 1) {
    ob1.Dispose();
    return 1;
    }
    // что-то еще сде­лать
    if (ob1.pro­perty1 == 2) {
    ob1.Dispose();
    return 2;
    }
    Чтобы не пи­сать каж­дый раз Dis­po­se, мож­но сде­лать так:
    
    using (MyClass ob1 = new MyClass()) {
    // что-то сде­лать с ob1
    if (ob1.pro­perty1 == 1) re­turn 1;
    // что-то еще сде­лать
    if (ob1.pro­perty1 == 2) re­turn 2;
    }
    
    Ситуации в ко­то­рых на­до ло­вить ис­к­лю­че­ния
    В об­щем ви­де это зву­чит как "Ло­вить ис­к­лю­че­ния на­до ког­да что-то мо­жет пой­ти не так". Ни о чем не го­во­ря­щая фра­за. Ес­ли быть бли­же к ре­аль­нос­ти - ло­вить ис­к­лю­че­ния на­до тог­да, ког­да вы не уве­ре­ны в об­ра­ба­ты­ва­емых дан­ных. Эта не­уве­рен­ность мо­жет быть по по­во­ду ти­па дан­ных, зна­че­ния дан­ных и т. д. Стан­дар­т­ные си­ту­ации, ког­да вы не мо­же­те быть уве­ре­ны:
    Все что ка­са­ет­ся вво­да дан­ных от поль­зо­ва­те­ля - ник­то не зна­ет что вве­дет поль­зо­ва­тель.
    Любое чте­ние из по­то­ка, ес­ли толь­ко это не по­ток в па­мя­ти - кто зна­ет что там в фай­ле на са­мом де­ле.
    Любая се­те­вая опе­ра­ция - вы не мо­же­те быть уве­ре­ны, что в нуж­ный мо­мент не обор­вет­ся ка­бель или си­сад­мин не ре­шит по­шу­тить.
    Любое об­ра­ще­ние к внеш­ним ба­зам дан­ных на­до рас­смат­ри­вать как се­те­вую опе­ра­цию.
    Любой вы­вод в по­ток - мес­то на дис­ке за­кан­чи­ва­ет­ся в са­мый не­под­хо­дя­щий мо­мент.
    
    Общие ме­то­ди­ки
    Исключения - не па­на­цея и не что-то очень хо­ро­шее, чем на­до час­то поль­зо­вать­ся. Они жрут по­ряд­ком ре­сур­сов, так что ре­ко­мен­ду­ет­ся про­ве­рять зна­че­ния ар­гу­мен­тов са­мим, а не по­ла­гать­ся на выб­рос ис­к­лю­че­ния.
    Пример:
    
    public string func1 (string inStr) {
    if (string.IsNul­lO­rEm­p­ty(inStr)) { re­turn null; }
    }
    такой код быс­т­рее, чем:
    
    public string func1 (string inStr) {
    if (string.IsNul­lO­rEm­p­ty(inStr) { throw new Ar­gu­men­tEx­cep­ti­on(); }
    }
    и уж, тем бо­лее, чем:
    
    public string func1 (string inStr) {
    try {
    // сде­лать что-то с inStr
    }
    catch (Argu­men­tEx­cep­ti­on ex) {
    throw ex;
    }
    }
    
    Теперь рас­ска­жу о раз­ных взгля­дах на об­щую тех­ни­ку. Лич­но я не счи­таю хоть один из этих ме­то­дов пра­виль­ным всег­да, им­хо - дей­ст­во­вать на­до по си­ту­ации. А ос­нов­ны­ми па­ра­мет­ра­ми, вли­я­ющи­ми на мой ме­тод от­ло­ва ис­к­лю­че­ний яв­ля­ют­ся раз­мер прог­рам­мы, сто­имость за­ка­за и вре­мя на соз­да­ние. Для пок­лон­ни­ков кор­по­ра­тив­но­го прог­рам­ми­ро­ва­ния хо­чу на­пом­нить - на­пи­са­ние всех воз­мож­ных оши­бок в прог­рам­ме вмес­те с ком­мен­та­ри­ями зай­мет вре­ме­ни боль­ше, чем соз­да­ние все­го ос­таль­но­го ко­да, без ин­тер­фей­са. И боль­шин­с­т­во кли­ен­тов хо­тят прог­рам­му быс­т­ро и де­ше­во, а не дол­го и до­ро­го, но с под­роб­ны­ми ошиб­ка­ми :). В от­ло­ве оши­бок глав­ное, им­хо, чтоб ин­фор­ма­ция об от­лов­лен­ной ошиб­ке бы­ла дос­та­точ­ной для вас, что­бы эту ошиб­ку най­ти и ис­п­ра­вить. А на поль­зо­ва­те­ля на­до вы­во­дить толь­ко! то, с чем он мо­жет спра­вить­ся сам - а это очень нем­но­го.
    
    Несколько ос­нов­ных спор­ных воп­ро­сов:
    1.Ловить каж­дый тип ис­к­лю­че­ния, или ло­вить все сра­зу. Как пра­ви­ло, опас­ный код мо­жет вы­ки­нуть не один тип ис­к­лю­че­ний­, а нес­коль­ко. Од­ни лю­ди го­во­рят, что ло­вить на­до обя­за­тель­но каж­дый тип от­дель­но, дру­гие го­во­рят, что на­до ло­вить все в од­ном бло­ке, а уж по­том раз­би­рать­ся что про­изош­ло. Им­хо, ес­ли ни с од­ним ти­пом ис­к­лю­че­ний поль­зо­ва­тель спра­вить­ся не мо­жет - то ло­вить на­до все сра­зу. Поль­зо­ва­те­лю все рав­но что слу­чи­лось - ошиб­ка хра­ни­мой про­це­ду­ры, пе­ре­пол­не­ние оче­ре­ди или еще ка­кая хрень - ему глав­ное знать, что "Тран­зак­ция не прош­ла. Поп­ро­буй­те еще раз.".
    2.Заключать в try - catch блок как мож­но мень­ше ко­да или как мож­но боль­ше. Мож­но соз­да­вать один блок try catch для всей фун­к­ции, а мож­но соз­да­вать по бло­ку на каж­дую по­тен­ци­аль­но опас­ную строч­ку. В об­щем и це­лом - это при­мер­но то же, что и пре­ды­ду­щий ва­ри­ант. Лич­но я счи­таю что за­со­вы­вать весь код фун­к­ции в try catch - бред. Хо­тя воз­мож­ны си­ту­ации ког­да это оп­рав­да­но. Но вот соз­да­вать по бло­ку на каж­дую строч­ку - это очень уж дол­го и нуд­но и ред­ко име­ет смысл.
    3.Надо ста­вить бло­ки catch в опас­ных строч­ках фун­к­ций и один catch где-то на са­мом вер­ху прог­рам­мы, или ста­вить гло­баль­ные от­лов­щи­ки на каж­дом уров­не или во­об­ще ни­ког­да не ста­вить гло­баль­ных catch бло­ков. Это, по­жа­луй­, са­мый спор­ный мо­мент в под­хо­дах. В при­ло­же­нии есть при­мер как сде­лать сов­ре­мен­ный гло­баль­ный пе­рех­ват­чик ис­к­лю­че­ний. Од­на­ко есть ряд лю­дей­, ко­то­рые счи­та­ют, что та­кой от­лов­щик - сви­де­тель­с­т­во пло­хо­го ко­да, мол хо­ро­ший код дол­жен от­лав­ли­вать все ошиб­ки там, где они воз­ни­ка­ют, а не на уров­не ma­in фун­к­ции прог­рам­мы. Воз­мож­но они и пра­вы, но я не сог­ла­сен. Еще один спор­ный мо­мент здесь - один гло­баль­ный пе­рех­ват­чик или нес­коль­ко на каж­дом струк­тур­ном уров­не... Ну это очень силь­но за­ви­сит от мас­ш­таб­нос­ти прог­рам­мы. Ес­ли прог­рам­ма ис­поль­зу­ет па­ру де­сят­ков биб­ли­отек, на­пи­сан­ных раз­ны­ми людь­ми и мо­жет за­ни­мать­ся од­нов­ре­мен­но нес­коль­ки­ми раз­ны­ми за­да­ча­ми - то нес­коль­ко пе­рех­ват­чи­ков име­ют смысл. Ес­ли прог­рам­ма од­но­по­точ­ная и на­пи­са­на од­ним прог­рам­мис­том - то вряд ли, од­но­го об­ще­го пе­рех­ват­чи­ка дол­ж­но хва­тить.
    
    И на­пос­ле­док - пом­ни­те, что пос­ле от­ло­ва ис­к­лю­че­ния прог­рам­ма час­то ос­та­ет­ся в не­ком про­ме­жу­точ­ном сос­то­янии - по­ло­ви­на зап­ро­шен­ных поль­зо­ва­те­лем дей­ст­вий вы­пол­не­на, вто­рая нет. Так что ра­бо­тать на­до ос­то­рож­но, что­бы не вой­ти в бес­ко­неч­ный цикл оши­бок. Осо­бен­но это ка­са­ет­ся еди­но­го пе­рех­ват­чи­ка. Ес­ли прог­рам­ма прос­тая, то в ко­де пе­рех­ват­чи­ка дол­ж­но быть при­ве­де­ние прог­рам­мы к ра­бо­че­му сос­то­янию. Ес­ли прог­рам­ма боль­шая и слож­ная - то вез­де, где по­яв­ле­ние ис­к­лю­че­ния гро­зит на­ру­ше­ни­ем хо­да прог­рам­мы дол­жен сто­ять свой ло­вец.
    
Гла­ва 4. Муль­ти­по­точ­ность.
    
    Мульти-поточность (mul­ti-thre­ading) - свой­ст­во прог­рамм вы­пол­нять нес­коль­ко за­дач од­нов­ре­мен­но. Ус­лов­но од­нов­ре­мен­но, ко­неч­но. В за­ви­си­мос­ти от ти­па про­цес­со­ра, од­нов­ре­мен­ность мо­жет быть ис­тин­ной и ими­ти­ру­емой. Впро­чем, для боль­шин­с­т­ва прог­рам­мис­тов эта раз­ни­ца не­су­щес­т­вен­на.
    Основными мо­мен­та­ми, зат­руд­ня­ющи­ми раз­ра­бот­ку муль­ти­по­точ­ных при­ло­же­ний яв­ля­ет­ся не­воз­мож­ность пред­с­ка­зать пос­ле­до­ва­тель­ность за­вер­ше­ния от­дель­ных по­то­ков на раз­ных ма­ши­нах. Да и от­лад­ка муль­ти­по­точ­ных при­ло­же­ний всег­да бы­ла го­лов­ной болью. Впро­чем, на VS 2005 вро­де бы с этим поп­ро­ще... по­ка у ме­ня все вро­де ра­бо­та­ло нор­маль­но. А вот 2003 VS глю­чил страш­но ес­ли раз­ные по­то­ки за­пус­ка­лись из раз­ных биб­ли­отек.
    
    При муль­ти­по­точ­ном вы­пол­не­нии ко­да ос­нов­ной проб­ле­мой яв­ля­ет­ся от­с­ле­жи­ва­ние дос­ту­пов из раз­ных по­то­ков к од­ной стра­ни­це па­мя­ти. Ни в ко­ем слу­чае не дол­ж­но по­лу­чить­ся так, что­бы один по­ток за­пи­сы­вал в пе­ре­мен­ную, ког­да дру­гой счи­ты­ва­ет из нее. След­с­т­вие из вы­шес­ка­зан­но­го - ес­ли у вас все ра­бо­та­ет нор­маль­но, т.е. по­то­ки не пе­ре­се­ка­ют­ся в од­ной пе­ре­мен­ной­, это не зна­чит, что они не пе­ре­се­кут­ся на дру­гой ма­ши­не. От­сю­да дру­гая проб­ле­ма - син­х­ро­ни­за­ция по­то­ков. Для ре­ше­ния этих проб­лем бы­ла раз­ра­бо­та­на тех­ни­ка се­ма­фо­ров и раз­лич­ных син­х­ро­ни­за­то­ров, ко­то­рая, в не­ко­то­рой сте­пе­ни ос­та­лась и в .net, од­на­ко те­перь прог­рам­мист прак­ти­чес­ки не име­ет с ней де­ла.
    
    Кратко о се­ма­фо­рах и син­х­ро­ни­за­ции.
    Синхронизация - ме­ха­низм, поз­во­ля­ющий по­то­кам кон­т­ро­ли­ро­вать соб­с­т­вен­ное вы­пол­не­ние от­но­си­тель­но друг дру­га. В об­щем и це­лом, син­х­ро­ни­за­ция ве­дет­ся че­рез фла­го­вые пе­ре­мен­ные и сход­на с об­щим ис­поль­зо­ва­ни­ем се­ма­фо­ров. По по­во­ду се­ма­фо­ров... ду­мал как луч­ше на­пи­сать, и ре­шил что кар­тин­ка бу­дет наг­ляд­нее:
    
    слева и спра­ва - по­то­ки, по ним пол­зет про­цесс вы­чис­ле­ния :). По цен­т­ру - пе­ре­мен­ная с се­ма­фо­ром. Ког­да ле­вый по­ток за­кан­чи­ва­ет счи­тать - он за­пи­сы­ва­ет дан­ные в пе­ре­мен­ную, по­том пра­вый по­ток при­хо­дит и счи­ты­ва­ет ее. Вот для то­го, что­бы пра­вый по­ток не счи­тал пе­ре­мен­ную, по­ка ле­вый ее не за­пи­шет и нуж­на син­х­ро­ни­за­ция... где-то еще дол­ж­на быть фла­го­вая (bo­ol) пе­ре­мен­ная, со сво­им се­ма­фо­ром, ко­то­рая бу­дет со­об­щать, за­кон­чил ли ле­вый по­ток счи­тать.
    
    Когда нуж­на муль­ти­по­точ­ность
    Самое рас­п­рос­т­ра­нен­ное ис­поль­зо­ва­ние муль­ти­по­точ­нос­ти - кноп­ка от­ме­на (или прер­вать) во вре­мя ка­ко­го-ли­бо про­цес­са - заг­руз­ки/сче­та/рен­де­ра и пр. Воб­щем-то, мож­но раз­де­лить ис­поль­зо­ва­ние мно­гих по­то­ков на две си­ту­ации - прос­тую и слож­ную :). Прос­тая - один по­ток счи­та­ет (или еще че­го де­ла­ет), вто­рой пе­ре­ри­со­вы­ва­ет фор­му, чтоб не ста­ла бе­лой­, про­ве­ря­ет не на­жа­та ли кноп­ка от­ме­на и ри­су­ет ин­ди­ка­тор прог­рес­са. Слож­ная - один по­ток на ин­тер­фей­се, а еще нес­коль­ко за­ня­ты сво­ими де­ла­ми... нап­ри­мер MS Word - один по­ток на ин­тер­фей­се, дру­гой на про­вер­ке вво­ди­мо­го, еще один на про­вер­ке об­нов­ле­ний­, еще один на про­вер­ке ор­фог­ра­фии, еще один на про­вер­ке грам­ма­ти­ки. Ор­фог­ра­фия и грам­ма­ти­ка син­х­ро­ни­зи­ро­ва­ны меж­ду со­бой и все по­то­ки син­х­ро­ни­зи­ро­ва­ны с ин­тер­фей­сным.
    
    Мультипоточность в .NET
    Те из вас, кто чи­тал msdn справ­ку по лю­бым клас­сам MS .NET на­вер­ня­ка ви­де­ли, что для боль­шин­с­т­ва клас­сов на­пи­са­но "thre­ad-sa­fe" - т.е. бе­зо­па­сен для муль­ти­по­точ­нос­ти. Ина­че го­во­ря, класс име­ет свой, встро­ен­ный се­ма­фор. На­до от­ме­тить, что в .NET все клас­сы име­ют свои се­ма­фо­ры, кро­ме тех, ко­то­рые ис­поль­зу­ют un­sa­fe код. В .NET 2.0 си­ту­ация нес­коль­ко из­ме­ни­лась, по срав­не­нию с .NET 1.1, в час­т­нос­ти, те­перь по­ми­мо се­ма­фо­ра, каж­дый объ­ект клас­са Con­t­rol (и все нас­лед­ни­ки) име­ет при­пис­ку к по­то­ку, в ко­то­ром он соз­дан, и об­ра­тить­ся к не­му из дру­го­го по­то­ка мож­но толь­ко че­рез ме­ха­низм De­le­ga­te In­vo­ke. Это поз­во­ля­ет лег­ко от­сечь по­тен­ци­аль­но опас­ные об­ра­ще­ния на ста­дии пер­вич­ной от­лад­ки. Ес­ли вы аб­со­лют­но уве­ре­ны, что по­то­ки не пе­ре­се­куть­ся, вы мо­же­те от­к­лю­чить этот ме­ха­низм - ус­та­но­ви­те свой­ст­во Chec­k­Fo­rIl­le­gal­C­ros­sTh­re­ad­Cal­ls объ­ек­та Con­t­rol рав­ным fal­se.
    
    Обращение к кон­т­ро­лу, соз­дан­но­му в дру­гом по­то­ке те­перь выг­ля­дит так:
    
    public class Form1 : Form
    {
    delegate vo­id Set­Tex­t­Cal­lback(string text);
    private Thre­ad de­moT­h­re­ad = null;
    private Tex­t­Box tex­t­Box1;
    private vo­id Set­Tex­t­Ma­in() {
    this.textBox1.Text = "This text was set un­sa­fely."; // не­бе­зо­пас­ная ус­та­нов­ка тек­с­та
    this.SetText(("This text was set sa­fely."); // бе­зо­пас­ная ус­та­нов­ка
    }
    private vo­id Set­Text(string text) {
    if (this.tex­t­Box1.Invo­ke­Re­qu­ired) { // ес­ли кон­т­рол в дру­гом по­то­ке
    SetTextCallback d = new Set­Tex­t­Cal­lback(Set­Text); // соз­дать вы­зов
    this.Invoke(d, new obj­ect[] { text }); // выз­вать фун­к­цию из дру­го­го по­то­ка
    }
    else { // ес­ли кон­т­рол в этом по­то­ке
    this.textBox1.Text = text; // ус­та­но­вить текст
    }
    }
    }
    
    Создание прос­той муль­ти­по­точ­нос­ти
    Для соз­да­ния прос­той и бе­зо­пас­ной муль­ти­по­точ­нос­ти в .NET 2.0 луч­ше все­го вос­поль­зо­вать­ся ком­по­нен­том Bac­k­g­ro­un­d­Wor­ker (фо­но­вый ра­бот­ник). Очень удоб­ная вещь - ком­по­нент, ко­то­рый уме­ет вы­пол­нять при­пи­сан­ную к не­му фун­к­цию в фо­но­вом ре­жи­ме, с под­дер­ж­кой "отме­ны" и прог­рес­са. Для всех фун­к­ций­, ко­то­рые не свя­за­ны с ин­тер­фей­сом и дол­ж­ны вы­пол­нять­ся фо­но­во - ре­ко­мен­дую поль­зо­вать­ся ра­бот­ни­ком.
    Пример ис­поль­зо­ва­ния в при­ло­же­нии.
    Если вам на­до за­пус­тить нес­коль­ко по­то­ков од­нов­ре­мен­но, но все они дол­ж­ны вы­пол­нять­ся фо­но­во - ис­поль­зуй­те его же. Нап­ри­мер, ес­ли вам на­до ска­чи­вать ку­чу фай­лов из се­ти, вы мо­же­те соз­дать с де­ся­ток ра­бот­ни­ков и пусть все они ка­ча­ют од­нов­ре­мен­но. Они все син­х­ро­ни­зи­ру­ют­ся, че­рез прог­ресс, с ос­нов­ным по­то­ком, но ни­как не свя­за­ны меж­ду со­бой.
    
    Сложная муль­ти­по­точ­ность
    Пару слов о слож­ной муль­ти­по­точ­нос­ти. Для на­ча­ла - а вы уве­ре­ны что оно вам на­до? Неп­рос­тое и му­тор­ное это де­ло. Но ес­ли вам так уж при­пер­ло за­пус­тить про­гу в ку­чу по­то­ков од­нов­ре­мен­но, да еще и свя­зан­ных меж­ду со­бой­, то де­ла­ет­ся это при­мер­но так:
    Есть класс Thre­ad (System.Thre­ading.Thre­ad). Он обес­пе­чи­ва­ет соб­с­т­вен­но по­то­ки. Есть клас­сы Thre­ad­S­tart и Pa­ra­me­te­ri­zed­T­h­re­ad­S­tart - они дер­жат ад­рес фун­к­ции ко­то­рую нуж­но за­пус­кать в по­то­ке, пер­вый - фун­к­ция дол­ж­на быть без ар­гу­мен­тов, вто­рой - с ар­гу­мен­том ти­па obj­ect.
    
    Синхронизация по­то­ков пол­нос­тью на вас, рав­но как ес­ли вам прип­рет стал­ки­вать по­то­ки лба­ми в объ­ек­те ва­ше­го клас­са - ва­ша за­да­ча от­с­ле­дить, что­бы не бы­ло на­ру­ше­ний... Каж­дая пе­ре­мен­ная в .NET thre­ad-sa­fe, но это не оз­на­ча­ет что класс це­ли­ком то­же thre­ad-sa­fe. Пред­с­тавь­те та­кую си­ту­ацию - вы за­пи­сы­ва­ете в класс пос­ле­до­ва­тель­но зна­че­ния пе­ре­мен­ных в од­ном по­то­ке, а дру­гой по­ток в это вре­мя их счи­ты­ва­ет. У вас да­же Ex­cep­ti­on не бу­дет - прос­то прог­рам­ма нач­нет глю­чить - по­ло­ви­на дан­ных из ста­ро­го сос­то­яния, вто­рая по­ло­ви­на из но­во­го... Что­бы это­го не слу­чи­лось - воз­в­ра­ща­ем­ся к ис­то­кам, де­ла­ем свои се­ма­фо­ры на каж­дый класс. Есть ку­ча раз­ных ме­то­дов, я по­ка­жу на очень прос­том при­ме­ре очень прос­той ме­тод.
    
    Создаем класс с се­ма­фо­ром (за­моч­ком):
    
    class Cal­c­Res
    {
    public bo­ol is­Loc­ked; // флаг за­пер­тос­ти
    private int loc­kId; // ID по­то­ка ко­то­рый за­пер
    private int _res; // пе­ре­мен­ная
    public int res { // свой­ст­во для пе­ре­мен­ной
    get {
    if (Thre­ad.Cur­ren­t­T­h­re­ad.Ma­na­ged­T­h­re­adId == loc­kId || loc­kId == 0) { // ес­ли за­мок не ус­та­нов­лен или ус­та­нов­лен этим по­то­ком
    return _res; // вер­нуть ре­зуль­тат
    }
    return 0; // или вер­нуть 0
    }
    set {
    if (Thre­ad.Cur­ren­t­T­h­re­ad.Ma­na­ged­T­h­re­adId == loc­kId || loc­kId == 0) { // ес­ли мож­но
    _res = va­lue; // ус­та­но­вить зна­че­ние
    }
    }
    }
    public Cal­c­Res() { // кон­с­т­рук­тор
    isLocked = fal­se; // не за­перт
    }
    public vo­id Lock() { // фун­к­ция За­пе­реть
    isLocked = true; // за­перт
    lockId = Thre­ad.Cur­ren­t­T­h­re­ad.Ma­na­ged­T­h­re­adId; // ус­та­но­вить ID по­то­ка
    }
    public vo­id Un­Lock() { // от­пе­реть
    if (Thre­ad.Cur­ren­t­T­h­re­ad.Ma­na­ged­T­h­re­adId == loc­kId) { // ес­ли id по­то­ков сов­па­да­ют
    lockId = 0; // снять id
    isLocked = fal­se; // снять за­мок
    }
    }
    }
    Простейшая им­п­ле­мен­та­ция клас­са с воз­мож­нос­тью за­пи­ра­ния на один по­ток. Те­перь по­тес­тим то, что у нас по­лу­чи­лось:
    Такая вот фор­ма:
    
    с та­ким вот ко­дом:
    
    public par­ti­al class Form1 : Form
    {
    Thread thre­ad1; // по­ток 1
    Thread thre­ad2; // по­ток 2
    CalcRes cal­c­Res; // класс для хра­не­ния ре­зуль­та­тов
    public Form1() {
    InitializeComponent();
    }
    delegate vo­id Set­Tex­t­Cal­lback(Tex­t­Box textb, string text); // де­ле­гат для за­пи­си в тек­с­то­вые по­ля
    private vo­id but­ton1_Click(obj­ect sen­der, Even­tArgs e) { // на­жа­тие кноп­ки
    if (but­ton1.Text == "but­ton1") { // ес­ли кноп­ку на­жа­ли пер­вый раз
    calcRes = new Cal­c­Res(); // соз­дать объ­ект клас­са
    thread1 = new Thre­ad(new Pa­ra­me­te­ri­zed­T­h­re­ad­S­tart(Cal­c­Func)); // соз­дать по­ток с при­пис­кой к фун­к­ции с па­ра­мет­ром
    thread2 = new Thre­ad(new Thre­ad­S­tart(Sta­te­Func)); // соз­дать по­ток без па­ра­мет­ров
    thread1.Start(1000000); // за­пус­тить по­ток 1 с ар­гу­мен­том 1000000
    thread2.Start(); // за­пус­тить вто­рой по­ток
    button1.Text = "stop"; // из­ме­нить текст на кноп­ке
    }
    else { // ес­ли на­жа­ли вто­рой раз
    thread1.Abort(); // прер­вать по­ток 1
    thread2.Abort(); // прер­вать по­ток 2
    button1.Text = "but­ton1"; // вос­ста­но­вить текст на кноп­ке
    }
    }
    void Cal­c­Func(obj­ect da­ta) { // фун­к­ция по­то­ка 1
    for (int i = 0; i ‹ (int)da­ta; i++) { // цикл по ар­гу­мен­ту
    for (int j = 0; j ‹ 100; j++) { // цикл на сот­ню - для наг­ляд­нос­ти
    if (cal­c­Res.isLoc­ked == fal­se) { // ес­ли объ­ект не за­перт
    calcRes.Lock(); // за­пе­реть
    calcRes.res++; // до­пи­сать зна­че­ние
    SetText(textBox1, cal­c­Res.res.ToS­t­ring()); // ус­та­но­вить текст
    calcRes.UnLock(); // от­пе­реть
    }
    Thread.Sleep(1); // по­дож­дать мил­ли­се­кун­ду
    }
    Thread.Sleep(1000); // по­дож­дать се­кун­ду
    }
    }
    void Sta­te­Func() { // фун­к­ция по­то­ка 2
    while (thre­ad1.IsA­li­ve) { // ес­ли пер­вая нить еще вы­пол­ня­ет­ся
    if (cal­c­Res.isLoc­ked == fal­se) { // ес­ли объ­ект не за­перт
    calcRes.Lock(); // за­пе­реть
    SetText(textBox2, cal­c­Res.res.ToS­t­ring()); // ус­та­но­вить текст
    calcRes.UnLock(); // от­пе­реть
    }
    else { // ес­ли за­перт
    SetText(textBox2, "loc­ked"); // на­пи­сать за­пер­то
    }
    SetText(textBox3, thre­ad1.Thre­ad­S­ta­te.ToS­t­ring()); // на­пи­сать сос­то­яние по­то­ка 1
    Thread.Sleep(10); // по­дож­дать 10 мил­ли­се­кунд
    }
    }
    void Set­Text(Tex­t­Box textb, string text) { // ус­та­нов­ка тек­с­та thre­ad-sa­fe
    if (tex­tb.Invo­ke­Re­qu­ired) {
    SetTextCallback d = new Set­Tex­t­Cal­lback(Set­Text);
    this.Invoke(d, new obj­ect[] { textb, text });
    }
    else {
    textb.Text = text;
    }
    }
    private vo­id Form1_For­m­C­lo­sing(obj­ect sen­der, For­m­C­lo­sin­gE­ven­tArgs e) { // при зак­ры­тии фор­мы
    if (thre­ad1.IsA­li­ve) thre­ad1.Abort(); // убить по­ток 1
    if (thre­ad2.IsA­li­ve) thre­ad2.Abort(); // убить по­ток 2
    }
    }
    Вообще-то еще на­до про­ве­рять соз­да­ны ли по­то­ки - при зак­ры­тии и при об­ра­ще­нии из дру­го­го по­то­ка. Но ес­ли на­до - са­ми до­ба­ви­те. Все ожи­да­ния - для ими­та­ции дол­гой и нап­ря­жен­ной ра­бо­ты :). Итак, ес­ли бы по­то­ки ни­ког­да не стал­ки­ва­лись и наш за­мок не ра­бо­тал, то над­пись loc­ked ни­ког­да бы не по­яви­лась во вто­ром тек­с­то­вом по­ле, а во вре­мя се­кун­д­ных па­уз пер­во­го по­то­ка в пер­вом тек­с­то­вом по­ле всег­да бы бы­ло чис­ло крат­ное 100. На кар­тин­ке по­ка­за­но сос­то­яние во вре­мя се­кун­д­ной па­узы.
    
Глава 5. Вызов функций библиотек Win32 (P/Invoke)
    
    Вызов фун­к­ций Win32 биб­ли­отек (p/invo­ke)
    
    Довольно час­тая си­ту­ация - вам нуж­на фун­к­ция из биб­ли­оте­ки, а она на­пи­са­на для Win32 и в .NET нап­ря­мую не под­к­лю­ча­ет­ся. Еще ча­ще - нуж­на сис­тем­ная фун­к­ция, а они все "спря­та­ны" в сис­тем­ных биб­ли­оте­ках, ко­то­рые все Win32. И нес­мот­ря на мно­го­чис­лен­ные обе­ща­ния Mic­ro­soft, сис­тем­ные биб­ли­оте­ки Vis­ta бу­дут то­же Win32. WinFX как был так и ос­та­ет­ся про­ек­том и меч­той.
    
    В язы­ках прог­рам­ми­ро­ва­ния до .NET бы­ли раз­лич­ные спо­со­бы им­пор­та фун­к­ций из dll. В .NET свой спо­соб, во мно­гом по­хо­жий на то, что бы­ло в с++ 6.0. Есть та­кая вот кон­с­т­рук­ция:
    
    [DllImport(‹имя_dll›, Ат­три­бу­ты)]
которая хранится в namespace System.Runtime.InteropServices. Аттрибуты - необязательны, имя dll - путь к dll, в кавычках.
    
    Аттрибуты вы­зо­ва есть сле­ду­ющие:
    CharSet - оп­ре­де­ля­ет как пе­ре­да­вать стро­ки (ANSI или Uni­co­de). Воз­мож­ные зна­че­ния An­si, Auto, No­ne, Uni­co­de. Ре­ко­мен­ду­ет­ся ста­вить Auto. Зна­че­ние это­го ат­три­бу­та вли­я­ет на по­иск фун­к­ций в сис­тем­ных биб­ли­оте­ках. Как вы зна­ете, у Mic­ro­soft'a стан­дарт - ес­ли фун­к­ция на­пи­са­на для An­si - в кон­це име­ни до­пи­сы­ва­ет­ся заг­лав­ная бук­ва А, ес­ли для uni­co­de - W. В ре­жи­ме Auto - ес­ли фун­к­ция с ука­зан­ным име­нем не най­де­на, сис­те­ма бу­дет ис­кать фун­к­ции с тем же име­нем, но с суф­фик­сом ко­ди­ров­ки. Для Win­dows95 - ищут­ся толь­ко фун­к­ции A, для всех ос­таль­ных - W. Что­бы сис­те­ма не ис­ка­ла фун­к­ции - ука­жи­те ат­три­бут Exac­t­S­pel­ling=true. Ес­ли Char­Set не ука­зан, в С# ста­вит­ся зна­че­ние по умол­ча­нию - An­si.
    ExactSpelling - по­ка­зы­ва­ет на­до ли ис­кать фун­к­ции с суф­фик­сом ко­ди­ров­ки. По умол­ча­нию = fal­se, т.е. ис­кать на­до.
    EntryPoint - наз­ва­ние или ад­рес фун­к­ции. Мож­но не за­да­вать, тог­да сис­те­ма бу­дет ис­кать фун­к­цию са­ма. Ис­поль­зо­вать име­ет смысл ког­да вам на­до пе­ре­име­но­вать фун­к­цию. Ну, нап­ри­мер фун­к­ция в биб­ли­оте­ке на­зы­ва­ет­ся "Fun­c­ti­onO­fAp­plyin­gAt­tri­bu­tes­To­Gi­ve­nO­bj­ec­t­s­Ver­si­on2Mo­del15", а вам та­кое длин­ное имя не нра­вит­ся, и вы хо­ти­те об­ра­щать­ся к фун­к­ции Ap­plyAt­tri­bu­tes. Вот тог­да вы в En­t­r­y­Po­int пи­ше­те длин­ное имя, а свою фун­к­цию на­зы­ва­ете так, как вам на­до.
    CallingConvention - по­ка­зы­ва­ет, как на­до об­ра­щать­ся с па­мятью при вы­зо­ве. Мо­жет при­ни­мать зна­че­ния - Cdecl, StdCall, This­Call, Wi­na­pi. Зна­че­ние по умол­ча­нию - Wi­na­pi, ко­то­рое рав­но StdCall на Win­dows ОС, и Cdecl на Win­dows CE. Cdecl ис­поль­зу­ет­ся для вы­зо­ва фун­к­ций с пе­ре­мен­ным чис­лом ар­гу­мен­тов. This­Call ис­поль­зу­ет­ся для вы­зо­ва фун­к­ций­, ра­бо­та­ющих с клас­са­ми, ко­то­рые то­же выз­ва­ны из Win32 dll.
    Остальные ат­три­бу­ты нуж­ны очень ред­ко. Под­роб­нее мож­но по­чи­тать здесь - http://msdn2.microsoft.com/en-US/library/system.runtime.interopservices.dllimportattribute(VS.80).aspx.
    
    Самое слож­ное в вы­зо­ве фун­к­ций из dll - пре­об­ра­зо­ва­ние ти­пов дан­ных. Ес­ли кто вдруг не зна­ет - до .NET бы­ли дру­гие мас­си­вы, не бы­ло спис­ков и пе­ре­чис­ле­ний­, а еще поч­ти все пе­ре­да­ва­лось че­рез ука­за­тель... так что рас­смот­рим как ка­кие ти­пы со­от­но­сят­ся.
    
    Простые ти­пы как пра­ви­ло так и пи­шут­ся: int, uint, byte.
    Таблица со­от­вет­с­т­вий прос­тых ти­пов тут - http://msdn2.microsoft.com/en-us/library/ac7ay120.aspx.
    
    Строки. Под­роб­нее здесь - http://msdn2.microsoft.com/en-us/library/e8w969hb.aspx.
    Со стро­ка­ми де­ло нес­коль­ко ху­же. Они пе­ре­да­ют­ся по раз­но­му, в за­ви­си­мос­ти от си­ту­ации:
    Если стро­ка вхо­дя­щий па­ра­метр - пе­ре­да­ет­ся как string, ес­ли фун­к­ция под­дер­жи­ва­ет Uni­co­de.
    
    [DllImport("User32.dll", En­t­r­y­Po­int="Mes­sa­ge­Box", Char­Set=Char­Set.Auto)]
    public sta­tic ex­tern int MsgBox(int hWnd, String text, String cap­ti­on, uint type);
    
 Если строка возвращаемый параметр - тоже просто string.
    Если стро­ка пе­ре­да­ет­ся как ука­за­тель, вхо­дя­щий па­ра­метр - Strin­g­Bu­il­der.
    
    [DllImport( "Ker­nel32.dll", Char­Set=Char­Set.Auto)]
    public sta­tic ex­tern int Get­S­y­s­tem­Di­rec­tory(Strin­g­Bu­il­der sysDir­Buf­fer, int si­ze);
    Если стро­ка воз­в­ра­ща­ет­ся как ука­за­тель - дек­ла­ри­ру­ет­ся как ука­за­тель, ко­то­рый по­том пе­ре­во­дит­ся в стро­ку.
    
    [ DllIm­port( "Ker­nel32.dll", Char­Set=Char­Set.Auto )] pub­lic sta­tic ex­tern IntPtr Get­Com­man­d­Li­ne();
    // ис­поль­зо­ва­ние
    IntPtr cmdLi­neStr = Get­Com­man­d­Li­ne();
    String com­man­d­Li­ne = Mar­s­hal.PtrToS­t­rin­gA­uto( cmdLi­neStr );
    
    Массивы. Под­роб­нее здесь - http://msdn2.microsoft.com/en-us/library/dd93y453.aspx.
    Массивы пе­ре­да­ют­ся нап­ря­мую. Вы ука­зы­ва­ете мо­ди­фи­ка­то­ры In, Out для ука­за­ния что имен­но мож­но с мас­си­вом де­лать - чи­тать (In) и/или пи­сать (Out).
    
    // фун­к­ция: int Tes­tAr­ra­yO­fIn­ts(int* pAr­ray, int pSi­ze);
    [ DllIm­port( "..\\LIB\\Pin­vo­ke­Lib.dll" )]
    public sta­tic ex­tern int Tes­tAr­ra­yO­fIn­ts([In, Out] int[] ar­ray, int si­ze );
    
    Если вам ну­жен ука­за­тель на мас­сив, нес­коль­ко слож­нее.
    
    //функция: int Tes­t­Re­fAr­ra­yO­fIn­ts(int** ppAr­ray, int* pSi­ze);
    [ DllIm­port( "..\\LIB\\Pin­vo­ke­Lib.dll" )]
    public sta­tic ex­tern int Tes­t­Re­fAr­ra­yO­fInts( ref IntPtr ar­ray, ref int si­ze );
    // ис­поль­зо­ва­ние
    int[] ar­ray2 = new int[10];
    int si­ze = ar­ray2.Length;
    IntPtr buf­fer = Mar­s­hal.Alloc­Co­Tas­k­Mem( Mar­s­hal.Si­ze­Of(si­ze) *array2.Length);
    Marshal.Copy( ar­ray2, 0, buf­fer, ar­ray2.Length );
    int sum2 = Tes­t­Re­fAr­ra­yO­fInts( ref buf­fer, ref si­ze );
    
    Если вам на­до пе­ре­дать мас­сив строк, т.е. ука­за­тель на мас­сив char, мож­но сде­лать так:
    
    // фун­к­ция: int Tes­tAr­ra­yOf­S­t­rin­gs(char** ppStrAr­ray, int si­ze);
    [ DllIm­port( "..\\LIB\\Pin­vo­ke­Lib.dll" )]
    public sta­tic ex­tern int Tes­tAr­ra­yOf­S­t­rings( [In, Out] String[] strin­gAr­ray, int si­ze );
    
    Если мас­сив пос­то­ян­ный (const), то его мож­но пе­ре­дать так:
    
    // па­ра­метр с++: TCHAR szCSDVer­si­on[ 128 ];
    [ Mar­s­ha­lAs( Un­ma­na­ged­T­y­pe.ByValTStr, Si­ze­Const=128 )] pub­lic String ver­si­on­S­t­ring;
    // па­ра­метр с++: int val[3];
    [ Mar­s­ha­lAs( Un­ma­na­ged­T­y­pe.ByVa­lAr­ray, Si­ze­Const=3 )] pub­lic int[] val;
    Конструкцию Mar­s­ha­lAs рас­смот­рим ни­же.
    
    Структуры и клас­сы. Под­роб­нее здесь - http://msdn2.microsoft.com/en-us/library/eshywdt7.aspx.
    Часто не­об­хо­ди­мо пе­ре­да­вать струк­ту­ры как ар­гу­мен­ты, в этом слу­чае дей­ст­ву­ем сле­ду­ющим об­ра­зом:
    
    //структура с:
    typedef struct _MYPER­SON
    {
    char* first;
    char* last; } MYPER­SON, *LP_MYPER­SON;
    // соз­да­ем свою струк­ту­ру
    [ Struc­t­La­yo­ut( La­yo­ut­Kind.Se­qu­en­ti­al, Char­Set=Char­Set.Ansi )]
    public struct MyPer­son {
    public String first;
    public String last;
    }
    Передаем струк­ту­ру как обыч­ный ар­гу­мент, ли­бо нап­ря­мую, ли­бо ука­за­тель на нее ис­поль­зуя сло­во ref. Па­ра­метр Char­Set в кон­с­т­рук­ции Struc­t­La­yo­ut ис­поль­зо­вать на­до так­же, как в кон­с­т­рук­ции DLLIm­port, т.е. ес­ли стро­ко­вые пе­ре­мен­ные есть - луч­ше ука­зы­вать ка­кие они имен­но эти стро­ки.
    Классы пе­ре­да­ют­ся так­же как струк­ту­ры. И для каж­дой с++ струк­ту­ры, вы мо­же­те соз­да­вать свой класс.
    
    //структура с++
    typedef struct _SYSTEM­TI­ME
    {
    WORD wYe­ar;
    WORD wMonth;
    WORD wDa­yOf­We­ek;
    WORD wDay;
    WORD wHo­ur;
    WORD wMi­nu­te;
    WORD wSe­cond;
    WORD wMil­li­se­conds; } SYSTEM­TI­ME, *PSYSTEM­TI­ME;
    // наш класс
    [ Struc­t­La­yo­ut( La­yo­ut­Kind.Se­qu­en­ti­al )]
    public class System­Ti­me {
    public us­hort ye­ar; …
    public us­hort mil­li­se­conds;
    }
    // ис­поль­зо­ва­ние
    SystemTime st = new System­Ti­me();
    GetSystemTime( st );
    
    Еще один мо­мент, ес­ли фун­к­ция биб­ли­оте­ки ожи­да­ет ука­за­тель на струк­ту­ру и од­ним из ва­ри­ан­тов мо­жет быть null - соз­да­вай­те класс, а не струк­ту­ру. Ес­ли ар­гу­мент фун­к­ции C# - класс, то null пе­ре­да­вать мож­но, ес­ли струк­ту­ра - нель­зя.
    
    Прочее. Под­роб­нее здесь - http://msdn2.microsoft.com/en-us/library/ss9sb93t.aspx.
    Есть еще не­ко­то­рые ти­пы дан­ных, ко­то­рые иног­да на­до пе­ре­да­вать и ко­то­рые весь­ма нес­тан­дар­т­но об­ра­ба­ты­ва­ют­ся.
    Указатель на фун­к­цию
    В С# ука­за­тель на фун­к­цию прев­ра­тил­ся в de­le­ga­te.
    
    // фун­к­ция с ++
    void Tes­t­Cal­lBack(FPTR pf, int va­lue);
    // дек­ла­ра­ция в C#
    public de­le­ga­te bo­ol FPtr( int va­lue );
    [ DllIm­port( "..\\LIB\\Pin­vo­ke­Lib.dll" )]
    public sta­tic ex­tern vo­id Tes­t­Cal­lBack( FPtr cb, int va­lue );
    
    HandleRef
    Хитрая вещь - все­го лишь уби­ра­ет ука­за­тель на объ­ект из спис­ка на уда­ле­ние, та­ким об­ра­зом поз­во­ляя пе­ре­да­вать объ­ект в биб­ли­оте­ку и те­рять ука­за­тель на не­го в ос­нов­ной прог­рам­ме без рис­ка, что объ­ект бу­дет уда­лен до то­го, как фун­к­ция биб­ли­оте­ки за­кон­чит­ся.
    
    // фун­к­ция с++
    bool Re­ad(Han­d­le hFi­le);
    // фун­к­ция C#
    [DLLImport("lib.dll")]
    public sta­tic ex­tern bo­ol Re­ad(Han­d­le­Ref hndRef);
    // ис­поль­зо­ва­ние
    FileStream fs = new Fi­leS­t­re­am( "Han­d­le­Ref.txt", Fi­le­Mo­de.Open );
    HandleRef hr = new Han­d­le­Ref( fs, fs.Han­d­le );
    Read(hr);
    
    LPARAM
    Часто встре­ча­ет­ся та­кой вот ар­гу­мент у сис­тем­ных фун­к­ций. Это ука­за­тель на па­ра­метр. Пе­ре­да­ет­ся та­ким вот об­ра­зом:
    
    // фун­к­ция с++
    BOOL En­su­re(LPA­RAM lPa­ram);
    // фун­к­ция C#
    [ DllIm­port( "lib.dll" )]
    public sta­tic ex­tern bo­ol En­su­re( IntPtr pa­ram );
    // ис­поль­зо­ва­ние
    TextWriter tw = System.Con­so­le.Out;
    GCHandle gch = GCHan­d­le.Alloc( tw );
    Ensure( (IntPtr)gch );
    gch.Free();
    
    void*
    Бывает та­кая вот вещь - ука­за­тель на пус­то­ту... Пе­ре­да­ет­ся так:
    
    // фун­к­ция с++
    void Set­Da­ta(vo­id* obj­ect);
    // фун­к­ция C#
    [ DllIm­port( "lib.dll" )]
    public sta­tic ex­tern vo­id Set­Da­ta( [Mar­s­ha­lAs(Unma­na­ged­T­y­pe.AsAny)] Obj­ect o);
    // ис­поль­зо­ва­ние
    SetData( (short)12 );
    SetData( (do­ub­le)12 );
    SetData( "abcd" );
    
    MarshalAs кон­с­т­рук­ция. Под­роб­нее здесь - http://msdn2.microsoft.com/en-US/library/system.runtime.interopservices.marshalasattribute.aspx.
    Эта кон­с­т­рук­ция поз­во­ля­ет ука­зать как имен­но дол­жен об­ра­ба­ты­вать­ся объ­ект. За­да­ет­ся так:
    
    MarshalAs(тип, ат­три­бу­ты)
    Аттрибуты не­обя­за­тель­ны. В ре­аль­ном ис­поль­зо­ва­нии я ви­дел толь­ко ат­три­бут Si­ze­Const, ука­зы­ва­ющий раз­мер пос­то­ян­но­го мас­си­ва.
    Тип, сог­лас­но ко­то­ро­му на­до об­ра­ба­ты­вать объ­ект, луч­ше все­го вы­би­рать из пе­ре­чис­ле­ния Un­ma­na­ged­T­y­pe (http://msdn2.microsoft.com/en-US/library/system.runtime.interopservices.unmanagedtype.aspx). Часть из пред­ло­же­ных ти­пов я уже ис­поль­зо­вал в при­ме­рах, про ос­таль­ные про­чи­та­ете са­ми - боль­но их мно­го.
    
Часть 3.
   Некоторые дополнительные осо­бен­нос­ти прог­рам­ми­ро­ва­ния под .net.    
Глава 1. Set­tings.
    
 
    У каж­дой­, бо­лее или ме­нее боль­шой прог­рам­мы есть ряд нас­т­ро­ек, ко­то­рые поль­зо­ва­тель мо­жет ме­нять. Ес­тес­т­вен­но, каж­до­му поль­зо­ва­те­лю хо­чет­ся, что­бы прог­рам­ма за­по­ми­на­ла его нас­т­рой­ки и ее не на­до бы­ло пе­ре­нас­т­ра­ивать за­но­во при каж­дом за­пус­ке. У не­ко­то­рых осо­бен­но боль­ших прог­рамм мо­гут быть соб­с­т­вен­ные нас­т­рой­ки, ко­то­рые ус­та­нав­ли­ва­ют­ся в мо­мент ин­с­тал­ля­ции или при пер­вом за­пус­ке, эти нас­т­рой­ки то­же луч­ше за­по­ми­нать, что­бы не нас­т­ра­ивать прог­рам­му при каж­дом за­пус­ке.
    
    Как за­по­ми­нать нас­т­рой­ки
    Стандартное ре­ше­ние - соз­дать класс для хра­не­ния па­ра­мет­ров нас­т­ро­ек и выг­ру­жать его со­дер­жи­мое в файл. Хоть вруч­ную, хоть се­ри­али­за­ци­ей. За­пи­сы­вать все нас­т­рой­ки в ре­естр - пло­хая при­выч­ка. Ес­ли на­до хра­нить нас­т­рой­ки для нес­коль­ких поль­зо­ва­те­лей - мож­но прос­то файл сох­ра­нять в ка­та­лог поль­зо­ва­те­ля, или на каж­до­го поль­зо­ва­тел­ся за­во­дить свой файл. На Вин­дах 2000 и поз­д­нее есть свои ка­та­ло­ги, на ос­таль­ных при­дет­ся сде­лать это вруч­ную. Лич­но я счи­таю, что для прог­рамм, не под­дер­жи­ва­ющих мно­го­поль­зо­ва­тель­ность, это оп­ти­маль­ный ва­ри­ант - сох­ра­нять класс нас­т­ро­ек в файл в род­ном ка­та­ло­ге прог­рам­мы. Но тут мо­жет быть проб­ле­ма не­сов­мес­ти­мос­ти вер­сий­, ко­то­рую по уму на­до всег­да ре­шать, од­на­ко ма­ло кто с этим свя­зы­ва­ет­ся. И ус­та­нов­ка но­вой вер­сии прог­рам­мы, как пра­ви­ло, при­во­дит к по­те­ре всех нас­т­ро­ек пре­ды­ду­щей вер­сии. Но эта проб­ле­ма лег­ко ре­ша­ет­ся - файл нас­т­ро­ек дол­жен со­дер­жать в на­ча­ле но­мер вер­сии, от ко­то­рой он соз­дан, а прог­рам­ма дол­ж­на счи­ты­вать нас­т­рой­ки в со­от­вет­с­т­вии с ука­зан­ной вер­си­ей. Нап­ри­мер, своя фун­к­ция счи­ты­ва­ния для каж­дой вер­сии фай­ла нас­т­ро­ек.
    
    Решение .NET 2.0 - ис­поль­зо­вать тех­но­ло­гию Set­tings. К со­жа­ле­нию, при всем удоб­с­т­ве, тех­но­ло­гия не до­ра­бо­та­на и не без глю­ков. В об­щем ви­де это выг­ля­дит так - в нас­т­рой­ках про­ек­та, сре­ди свойств, есть вклад­ка Set­tings. Там вы мо­же­те за­да­вать па­ра­мет­ры. Я уже по­ка­зы­вал как это де­лать, но в об­щем ви­де, это при­мер­но так:
    
    Первый стол­бец - имя па­ра­мет­ра, вто­рой - тип, тре­тий - груп­па, чет­вер­тый - зна­че­ние. Тип па­ра­мет­ра мо­жет быть поч­ти лю­бым - прос­тые ти­пы дан­ных, вклю­чая string, и спе­ци­аль­ный - кол­лек­ция string, а так­же нес­коль­ко клас­сов ри­со­ва­ния, con­nec­ti­on string, и все что угод­но че­рез ва­ри­ант Brow­se. Един­с­т­вен­ное ус­ло­вие - тип дол­жен иметь или ToS­t­ring/From­S­t­ring фун­к­ции для Type­Con­ver­ter, или быть xmlSe­ri­ali­zab­le. Груп­па мо­жет быть или User - нас­т­рой­ки сох­ра­ня­ют­ся для каж­до­го поль­зо­ва­те­ля, и мо­гут быть им из­ме­не­ны, или Ap­pli­ca­ti­on - нас­т­рой­ки не мо­гут быть из­ме­не­ны поль­зо­ва­те­лем. Смысл груп­пы Ap­pli­ca­ti­on - ес­ли нуж­ны кон­с­тан­ты, ко­то­рые не мо­гут быть за­да­ны прог­рам­мис­том (нап­ри­мер, па­ра­мет­ры ком­па или ти­па то­го) и за­да­ют­ся при ин­с­тал­ля­ции прог­рам­мы, что­бы не вы­чис­лять па­ра­мет­ры при каж­дом за­пус­ке - они за­пи­сы­ва­ют­ся в та­кие вот свой­ст­ва. Впро­чем, в эти свой­ст­ва мож­но за­пи­сать и обыч­ные кон­с­тан­ты.
    Основные пре­иму­щес­т­ва - Set­tings са­ми от­с­ле­жи­ва­ют вер­сию прог­рам­мы и поль­зо­ва­те­ля. Есть встро­ен­ные фун­к­ции сох­ра­не­ния, заг­руз­ки и воз­в­ра­та к стан­дар­т­ным нас­т­рой­кам. Мож­но за­вя­зать кон­т­ро­лы нап­ря­мую на set­tings. Са­ми заг­ру­жа­ют­ся при за­пус­ке, са­ми сох­ра­ня­ют­ся при зак­ры­тии, са­ми за­пол­ня­ют­ся ес­ли свя­за­ны с кон­т­ро­лом.
    Основные не­дос­тат­ки - на каж­дую вер­сию соз­да­ет­ся свой файл, что из­ряд­но за­со­ря­ет ка­та­лог поль­зо­ва­те­ля, ес­ли вер­сии вы­хо­дят дос­та­точ­но час­то. Кста­ти, стан­дар­т­ным де­ин­с­тал­ля­то­ром нас­т­рой­ки не уда­ля­ют­ся. Воз­в­рат к стан­дар­т­ным нас­т­рой­кам и сох­ра­не­ние - иног­да глю­чат. Связ­ка кон­т­ро­лов с па­ра­мет­ром - мо­жет по­рож­дать глю­ки. Все зна­че­ния нас­т­ро­ек сох­ра­ня­ют­ся ли­бо в тек­с­то­вом фор­ма­те, ли­бо че­рез xmlSe­ri­ali­zer, что иног­да силь­но уве­ли­чи­ва­ет объ­ем.
    
    Общая схе­ма ра­бо­ты с Set­tings при­мер­но та­кая:
    Если вы не свя­зы­ва­ете кон­т­ро­лы с set­tings:
    Задаете свой­ст­ва в ди­зай­не­ре, при заг­руз­ке фор­мы - заг­ру­жа­ете set­tings и пе­ре­пи­сы­ва­ете зна­че­ния свойств в кон­т­ро­лы, при зак­ры­тии фор­мы - пе­ре­пи­сы­ва­ете зна­че­ния из кон­т­ро­лов в set­tings и сох­ра­ня­ете их.
    Это строч­ка об­ра­ще­ния к пос­лед­ним сох­ра­нен­ным set­tings:
    
    namespace.Properties.Settings.Default
    Вместо "na­mes­pa­ce" - под­с­тав­те тот na­mes­pa­ce в ко­то­ром вы ра­бо­та­ете.
    Загрузка нас­т­ро­ек, мож­но пря­мо за­вес­ти по­ле в фор­ме:
    
    namespace.Properties.Settings sets = na­mes­pa­ce.Pro­per­ti­es.Set­tin­gs.De­fa­ult;
    
    Переписывание зна­че­ний в кон­т­ро­лы:
    
    textBox1.Text = sets.Tex­t­F­rom­Tex­t­box1;
    
    Переписывание зна­че­ний из кон­т­ро­лов:
    
    sets.TextFromTextbox1 = tex­t­Box1.Text;
    
    Сохранение нас­т­ро­ек:
    
    sets.Save();
    
    В иде­але, ес­ли кон­т­ро­лы свя­за­ны с нас­т­рой­ка­ми - мо­же­те ни­че­го не де­лать. Все сде­ла­ют за вас, но есть нес­коль­ко мо­мен­тов, из-за ко­то­рых до сих пор удоб­нее рас­ши­рен­ные нас­т­рой­ки де­лать вруч­ную.
    Все кон­т­ро­лы дол­ж­ны быть с им­п­ле­мен­ти­ро­ван­ным IBin­dab­le­Com­po­nent ин­тер­фей­сом. Со­от­вет­с­т­вен­но, ес­ли вы поль­зу­етесь сво­им кон­т­ро­лом - ин­тер­фей­с им­п­ле­мен­ти­ро­вать бу­де­те са­ми. Ес­ли кон­т­рол без ин­тер­фей­са - пе­ре­пи­сы­ва­ние зна­че­ний и заг­руз­ка опять ля­гут на вас.
    Некоторые кон­т­ро­лы из-за ав­то­ма­ти­чес­кой син­х­ро­ни­за­ции с set­tings глю­чат, и их все рав­но при­хо­дит­ся об­ра­ба­ты­вать вруч­ную.
    
    Для удоб­с­т­ва уп­рав­ле­ния есть еще 4 со­бы­тия клас­са set­tings:
    SettingsLoaded - сра­ба­ты­ва­ет, ког­да заг­ру­жа­ют­ся свой­ст­ва. Ре­ко­мен­ду­ет­ся ис­поль­зо­вать для про­вер­ки пра­виль­нос­ти на­чаль­ных зна­че­ний.
    SettingChanged - сра­ба­ты­ва­ет до из­ме­не­ния зна­че­ния свой­ст­ва. Ре­ко­мен­ду­ет­ся для про­вер­ки вве­ден­но­го зна­че­ния на пра­виль­ность.
    PropertyChanged - не ре­ко­мен­до­ва­но к ис­поль­зо­ва­нию, ес­ли не тре­бу­ет­ся про­вер­ка вве­ден­но­го свой­ст­ва в от­дель­ном по­то­ке.
    SettingsSaved - сра­ба­ты­ва­ет пе­ред за­писью свойств в файл. Ре­ко­мен­ду­ет­ся ис­поль­зо­вать для про­вер­ки пра­виль­нос­ти свойств пе­ред сох­ра­не­ни­ем, и для до­пол­ни­тель­ной син­х­ро­ни­за­ции зна­че­ний.
    
    Итого, set­tings хо­ро­шо ис­поль­зо­вать ес­ли у вас нет соб­с­т­вен­ных кон­т­ро­лов, ко­то­рые на­до свя­зы­вать, ес­ли все кон­т­ро­лы, ко­то­рые вы ис­поль­зу­ете не глю­чат и ес­ли вер­сии ва­шей прог­рам­мы вы­хо­дят ред­ко. Тог­да это удоб­но, мож­но че­рез ди­зай­нер за­вя­зать ку­чу па­ра­мет­ров внеш­не­го ви­да в нас­т­рой­ки, и бу­дет у вас кле­вая про­га, ко­то­рая пом­нит в ка­ком мес­те эк­ра­на ее зак­ры­ли :).
    
    Основное на этом за­кан­чи­ва­ет­ся. Ос­таль­ное - вся­кие при­ко­лы, а-ля соб­с­т­вен­ные клас­сы для свойств, для об­ра­бот­чи­ка свойств и пр.
    
Гла­ва 2. Се­ри­али­за­ция.
    
    Сериализация объ­ек­тов - про­цесс пе­ре­во­да соз­дан­но­го объ­ек­та клас­са в уни­вер­саль­ный по­ток байт, ко­то­рый мо­жет быть по­том пре­об­ра­зо­ван об­рат­но в объ­ект, с по­мощью де­се­ри­али­за­ции. Та­кое вот об­щее оп­ре­де­ле­ние. Пос­коль­ку тех­ни­ка уни­вер­саль­ная, бо­лее кон­к­рет­но ни­че­го ска­зать не по­лу­чить­ся. Рас­смот­рим на при­ме­ре, мо­жет так яс­нее бу­дет:
    
    Пример 1. Пред­по­ло­жим вы пи­ше­те прог­рам­му для ра­бо­ты с ди­аг­рам­ма­ми, а-ля Vi­sio. Ито­гом ра­бо­ты поль­зо­ва­те­ля бу­дет изоб­ра­же­ние - рас­т­ро­вое, век­тор­ное - не важ­но. Но про­ме­жу­точ­ный ре­зуль­тат ра­бо­ты - это имен­но ди­аг­рам­ма, т.е. вся­кие фор­мы, стрел­ки, текст, все это име­ет па­ра­мет­ры ти­па ко­ор­ди­нат, цве­та и пр. Поль­зо­ва­тель, ра­зу­ме­ет­ся, за­хо­чет сох­ра­нить свою ра­бо­ту, при­чем сох­ра­нить так, что­бы мож­но бы­ло по­том ре­дак­ти­ро­вать. Как вы пос­ту­пи­те? Ну, ско­рее все­го, соз­да­ди­те свой фор­мат фай­ла и бу­де­те ту­да пос­ле­до­ва­тель­но выг­ру­жать все, что поль­зо­ва­тель на­соз­да­вал. Вот тут-то вам и по­на­до­бить­ся се­ри­али­зи­ция - вы мо­же­те выг­ру­жать объ­ек­ты клас­са, вмес­те со все­ми свой­ст­ва­ми ав­то­ма­ти­чес­ки. Со­от­вет­с­т­вен­но, ког­да поль­зо­ва­тель за­хо­чет заг­ру­зить ра­бо­ту - вы за­пус­ка­ете де­се­ри­али­за­цию.
    
    Пример 2. Пред­по­ло­жим, у вас есть прог­рам­ма, сос­то­ящая из двух час­тей - кли­ен­т­с­кой и сер­вер­ной. Поль­зо­ва­тель ра­бо­та­ет на кли­ен­те, а по­том сох­ра­ня­ет ра­бо­ту на сер­ве­ре. Вам на­до пе­ре­дать все, что он на­ра­бо­тал на сер­вер... Как имен­но пе­ре­да­вать - не суть важ­но, в лю­бом слу­чае, пе­ре­дать вы мо­же­те толь­ко по­ток байт, зна­чит сна­ча­ла, на­до при­вес­ти ра­бо­ту поль­зо­ва­те­ля к это­му по­то­ку. Се­ри­али­за­ция имен­но для это­го и де­ла­лась - прос­то, на сей раз, вы выг­ру­жа­ете ра­бо­ту не в файл, а в се­те­вой по­ток. А сер­вер­ная часть, по­лу­чив по­ток, де­се­ри­али­зи­ру­ет его и за­пи­сы­ва­ет ку­да ей на­до.
    
    Насчет где ис­поль­зу­ет­ся мож­но счи­тать я ска­зал. В при­ме­рах вы­ше два ос­нов­ных ис­поль­зо­ва­ния тех­ни­ки. В об­щем ви­де это зву­чит как "исполь­зу­ет­ся при не­об­хо­ди­мос­ти пе­ре­дать дан­ные об объ­ек­те клас­са меж­ду дву­мя при­ло­же­ни­ями, уме­ющи­ми с этим клас­сом ра­бо­тать". Хо­тя, это не сов­сем вер­но, пос­коль­ку мож­но и не уметь ра­бо­тать с клас­сом, прос­то тог­да не по­нят­но, за­чем это на­до?..
    
    Как поль­зо­вать­ся
    Очень прос­то - лю­бой класс, ко­то­рый име­ет ат­три­бут [Se­ri­ali­zab­le] и фун­к­цию Ge­tO­bj­ec­t­Da­ta, мо­жет быть се­ри­али­зо­ван. Ес­ли у клас­са есть де­се­ри­али­за­ци­он­ный кон­с­т­рук­тор - он мо­жет быть де­се­ри­али­зо­ван. Боль­шин­с­т­во клас­сов .net под­дер­жи­ва­ют се­ри­али­за­цию. Об­ра­ти­те вни­ма­ние - ког­да вы смот­ри­те опи­са­ние клас­са в msdn - у мно­гих клас­сов есть при­пис­ка ISe­ri­ali­zab­le, IX­m­l­Se­ri­ali­zab­le, а пе­ред наз­ва­ни­ем клас­са сто­ит [Se­ri­ali­zab­le­At­tri­bu­te] (Нап­ри­мер, Da­ta­Tab­le). У струк­тур (нап­ри­мер, Po­int) есть толь­ко ат­три­бут. Им ин­тер­фей­с не ну­жен.
    Как сде­лать свой класс се­ри­али­зи­ру­емым:
    
    [Serializable()]
    [ComVisibleAttribute(false)]
    public class Se­ri­ali­zab­leC­lass : ISe­ri­ali­zab­le
    {
    public Se­ri­ali­zab­leC­lass() {
    dataField = 0;
    dataField2 = 0.0;
    }
    private int da­ta­Fi­eld;
    private do­ub­le da­ta­Fi­eld2;
    public vir­tu­al vo­id Ge­tO­bj­ec­t­Da­ta(Se­ri­ali­za­ti­onIn­fo in­fo, Stre­amin­g­Con­text con­text) {
    // до­бав­ля­ем пе­ре­мен­ные в спи­сок на сох­ра­не­ние
    info.AddValue("dataField", da­ta­Fi­eld);
    info.AddValue("dataField2", da­ta­Fi­eld2);
    }
    }
    
    Как вклю­чить де­се­ри­али­за­цию - до­ба­вить кон­с­т­рук­тор:
    
    public Se­ri­ali­zab­leC­lass(Se­ri­ali­za­ti­onIn­fo in­fo, Stre­amin­g­Con­text con­text) : this() {
    dataField = in­fo.Ge­tInt32("da­ta­Fi­eld");
    dataField2 = in­fo.Get­Do­ub­le("da­ta­Fi­eld2");
    }
    
    А мож­но де­се­ри­али­за­цию сде­лать уни­вер­саль­ной­, ис­поль­зуя Ref­lec­ti­on:
    
    public Se­ri­ali­zab­leC­lass(Se­ri­ali­za­ti­onIn­fo in­fo, Stre­amin­g­Con­text con­text) : this() {
    SerializationInfoEnumerator en = in­fo.Ge­tE­nu­me­ra­tor();
    en.MoveNext();
    for (int i = 0; i ‹ in­fo.Mem­ber­Co­unt; i++) {
    this.GetType().InvokeMember(en.Current.Name, Bin­din­g­F­lags.Instan­ce | Bin­din­g­F­lags.Pub­lic | Bin­din­g­F­lags.Non­Pub­lic | Bin­din­g­F­lags.Set­P­ro­perty, null, this, new obj­ect[1] { en.Cur­rent.Va­lue });
    en.MoveNext();
    }
    }
    
    Сериализацию то­же мож­но сде­лать уни­вер­саль­ной­, по су­ти, ес­ли не им­п­ле­мен­ти­ро­вать ин­тер­фей­с - се­ри­али­за­тор бу­дет ста­рать­ся выг­ру­зить все чле­ны клас­са... Но, как пра­ви­ло, в клас­се на­хо­дить­ся нес­коль­ко чле­нов без ат­три­бу­та Se­ri­ali­zab­le, и се­ри­али­за­тор вы­ки­ды­ва­ет ошиб­ку. По­это­му, ча­ще все­го, при­хо­дить­ся чле­ны на за­пись за­би­вать вруч­ную, че­рез Ge­tO­bj­ec­t­Da­ta.
    
    Как за­пус­тить се­ри­али­за­цию:
    Предположим, у нас в клас­се вся нуж­ная ин­фо­рам­ция хра­нить­ся в кол­лек­ции Da­ta­Obj­ects, яв­ля­ющей­ся кол­лек­ци­ей объ­ек­тов ти­па Se­ri­ali­zab­leC­lass. Так­же, пред­по­ло­жим, у Se­ri­ali­zab­leC­lass есть event mo­use­Down. Event'ы не сох­ра­ня­ют­ся, ес­тес­т­вен­но, по­это­му при де­се­ри­али­за­ции, на­до event опять ус­та­нав­ли­вать.
    
    internal vo­id Se­ri­ali­ze(string fi­le­na­me) {
    Stream stre­am;
    IFormatter for­mat­ter = new Bi­nar­y­For­mat­ter();
    try {
    stream = new Fi­leS­t­re­am(fi­le­na­me, Fi­le­Mo­de.Cre­ate, Fi­le­Ac­cess.Wri­te, Fi­leS­ha­re.No­ne);
    }
    catch (Excep­ti­on ex) { throw ex; }
    try {
    for (int i = 0; i ‹ Da­ta­Obj­ec­ts.Co­unt; i++) {
    formatter.Serialize(stream, Da­ta­Obj­ec­ts[i]);
    }
    }
    catch (Excep­ti­on ex) { throw ex; }
    finally { stre­am.Clo­se(); }
    }
    
    Как за­пус­тить де­се­ри­али­за­цию:
    
    internal vo­id De­Se­ri­ali­ze(string fi­le­na­me, Mo­use­Even­t­Han­d­ler mo­use­Down) {
    IFormatter for­mat­ter = new Bi­nar­y­For­mat­ter();
    Stream stre­am;
    try {
    stream = new Fi­leS­t­re­am(fi­le­na­me, Fi­le­Mo­de.Open, Fi­le­Ac­cess.Re­ad, Fi­leS­ha­re.Re­ad);
    }
    catch (Excep­ti­on ex) {
    throw ex;
    }
    object obj;
    try {
    while (stre­am.Po­si­ti­on ‹ stre­am.Length-1) {
    obj = for­mat­ter.De­se­ri­ali­ze(stre­am);
    // ус­та­нав­ли­ва­ем event, ес­ли на­до
    ((SerializableClass)obj).MouseDown += mo­use­Down;
    // вно­сим объ­ект в прог­рам­му
    DataObjects.Add((SerializableClass)obj);
    }
    }
    catch (Excep­ti­on ex) {
    throw ex;
    }
    finally { stre­am.Clo­se(); }
    }
    
    Немного о ти­пах се­ри­али­за­ции - есть два ос­нов­ных ти­па: би­нар­ный и xml. Пер­вый ком­пак­т­нее, вто­рой уни­вер­саль­нее. Лич­но я ни­ког­да xml се­ри­али­за­ци­ей не поль­зо­вал­ся, и счи­таю ее ма­ло нуж­ной вещью... мож­но спо­рить, ко­неч­но, но на мой взгляд, пред­ла­га­емая уни­вер­саль­ность до­воль­но мни­мая вещь.
    Помимо это­го вы мо­же­те сде­лать свой се­ри­али­за­тор, за­да­ча дол­гая, нуд­ная, но мо­жет при­нес­ти не­ма­лые вы­го­ды. Для справ­ки: се­ри­али­за­ция Int32 за­ни­ма­ет 54 бай­та, да­же в би­нар­ном ви­де, пос­коль­ку 50 байт ухо­дят на наз­ва­ние клас­са. Так что иног­да луч­ше поль­зо­вать­ся сво­ими се­ри­али­за­то­ра­ми.
    
    Итоги:
    Основные плю­сы се­ри­али­за­ции - удоб­с­т­во для прог­рам­мис­та. По­ми­мо прос­то­ты на­пи­са­ния ко­да, там еще есть ме­ха­низм от­с­ле­жи­ва­ния вер­сий­, нап­ри­мер.
    Основные ми­ну­сы - боль­шие объ­емы, и не­об­хо­ди­мость час­то вруч­ную пе­ре­чис­лять тех, ко­го на­до выг­ру­жать.
    
Гла­ва 3. Ref­lec­ti­on.
    Перевод с ком­мен­та­ри­ями статьи с codeproject.com: http://www.codeproject.com/csharp/introreflection.asp. Это не пол­ный пе­ре­вод статьи, а толь­ко пе­рес­каз тех мест, ко­то­рые я счел са­мы­ми важ­ны­ми.
    
    Введение
    Reflection - зна­чи­тель­ное но­вов­ве­де­ние в .NET. Че­рез Ref­lec­ti­on прог­рам­мы со­би­ра­ют и ра­бо­та­ют со сво­ими ме­та­дан­ны­ми. Это мощ­ный ме­ха­низ для изу­че­ния as­sembly и раз­ных объ­ек­тов во вре­мя ра­бо­ты прог­рам­мы. Рас­смат­ри­ва­емое API на­хо­дит­ся в System.Ref­lec­ti­on na­mes­pa­ce. Че­рез Ref­lec­ti­on мож­но по­лу­чить ин­фор­ма­цию о клас­сах, ме­то­дах, свой­ст­вах и со­бы­ти­ях лю­бо­го объ­ек­та, а так­же мож­но вы­зы­вать ме­то­ды. И т.д.
    
    Использование Ref­lec­ti­on для по­лу­че­ния спис­ка ис­поль­зу­емых as­sembly
    
    
    using System;
    using System.Ref­lec­ti­on;
    namespace Ref­lec­ti­on­De­moC­S­harp
    {
    class Re­fe­ren­ce­dAs­sem­b­li­es
    {
    [STAThread]
    static vo­id Ma­in(string[] args)
    {
    Assembly[] ap­pAs­sem­b­li­es =
    System.AppDomain.CurrentDomain.GetAssemblies ();
    foreach (Assembly as­sembly in ap­pAs­sem­b­li­es )
    {
    Console.WriteLine (assem­b­ly.Ful­lNa­me );
    }
    Console.ReadLine ();
    }
    }
    }
    
    Вывод будет таким:
    
    mscorlib, Ver­si­on=1.0.5000.0, Cul­tu­re=ne­ut­ral, Pub­lic­Key­To­ken=b77a5c561934e089
    ReflectionDemoCSharp, Ver­si­on=1.0.1882.29904, Cul­tu­re=ne­ut­ral, Pub­lic­Key­To­ken=null
    
    Класс System.AppDo­ma­in class пред­с­тав­ля­ет до­мен при­ло­же­ния, ко­то­рый яв­ля­ет­ся не­ко­ей изо­ли­ро­ван­ной об­лас­тью, в ко­то­рой ис­пол­ня­ет­ся при­ло­же­ние.
    Надо по­яс­нить, что это изо­ли­ро­ван­ная об­ласть па­мя­ти, по­то­ков, про­цес­сов и про­че­го. Для каж­до­го при­ло­же­ния соз­да­ет­ся свой до­мен, и все не­об­хо­ди­мые as­sembly заг­ру­жа­ют­ся в до­мен каж­до­го при­ло­же­ния. Для об­ще­ния меж­ду до­ме­на­ми есть свои осо­бые ме­ха­низ­мы.
    Метод Ge­tAs­sem­b­li­es() воз­в­ра­ща­ет спи­сок as­sembly, заг­ру­жен­ных в до­мен Ref­lec­ti­on­De­moC­S­harp.
    
    Исследование as­sembly и по­лу­че­ние спис­ка ти­пов
    Для на­ча­ла, ди­на­ми­чес­ки заг­ру­жа­ем as­sembly, че­рез As­sem­b­ly.Lo­ad().
    
    public sta­tic As­sem­b­ly.Lo­ad(Assem­b­l­y­Na­me)
    Передаем MsCor­Lib.dll.
    
    Assembly Lo­ade­dAs­sembly = As­sem­b­ly.Lo­ad("mscor­lib.dll");
    
    Когда as­sembly заг­ру­же­на, мо­жем вос­поль­зо­вать­ся ме­то­дом Get­T­y­pes(), что­бы по­лу­чить мас­сив ти­пов.
    
    System.Type[] Exis­tin­g­T­y­pes = Lo­ade­dAs­sem­b­ly.Get­T­y­pes ();
    Возвращаемые ти­пы мо­гут пред­с­тав­лять клас­сы, ин­тер­фей­сы или пе­ре­чис­ле­ния.
    
    using System;
    using System.Ref­lec­ti­on ;
    namespace Ref­lec­ti­on­De­moC­S­harp
    {
    class Ref­lec­ted­T­y­pes
    {
    [STAThread]
    static vo­id Ma­in(string[] args)
    {
    Assembly Lo­ade­dAs­sembly = As­sem­b­ly.Lo­ad ("mscor­lib.dll");
    System.Type[] Exis­tin­g­T­y­pes = Lo­ade­dAs­sem­b­ly.Get­T­y­pes ();
    foreach(Type type in Exis­tin­g­T­y­pes)
    Console.WriteLine (type.ToS­t­ring ());
    Console.WriteLine (Exis­tin­g­T­y­pes.Length +
    " Types Dis­co­ve­red in mscor­lib.dll");
    Console.ReadLine ();
    }
    }
    }
    
    Вывод (час­тич­ный­):
    
    System.Object
    System.ICloneable
    System.Collections.IEnumerable
    System.Collections.ICollection
    System.Collections.IList
    System.Array
    System.Array+SorterObjectArray
    System.Array+SorterGenericArray
    System.Collections.IEnumerator
    1480 Types Dis­co­ve­red in mscor­lib.dll
    
    Исследование ти­па
    Попробуем по­лу­чить спи­сок всех чле­нов кон­к­рет­но­го ти­па. Ме­тод Type.Get­T­y­pe(Type­Na­me) воз­в­ра­ща­ет объ­ект System.Type, со­от­вет­с­т­ву­ющий стро­ке ар­гу­мен­та. Мы зап­ро­сим тип с по­мощью ме­то­да Type.Get­Mem­bers(), что­бы по­лу­чить мас­сив его чле­нов.
    Я уже рас­ска­зы­вал, что вмес­то Type.Get­T­y­pe("System.Int32") мож­но ис­поль­зо­вать type­of(int).
    
    using System;
    using System.Ref­lec­ti­on ;
    namespace Ref­lec­ti­on­De­moC­S­harp
    {
    class Ref­lec­ted­T­y­pes
    {
    [STAThread]
    static vo­id Ma­in(string[] args)
    {
    Type Type­To­Ref­lect = Type.Get­T­y­pe("System.Int32");
    System.Reflection.MemberInfo[] Mem­bers =type.Get­Mem­bers();
    Console.WriteLine ("Mem­bers of "+Type­To­Ref­lect.ToS­t­ring ());
    Console.WriteLine();
    foreach (Mem­be­rIn­fo mem­ber in Mem­bers )
    Console.WriteLine(member);
    Console.ReadLine ();
    }
    }
    }
    Вывод:
    
    Members of System.Int32
    Int32 Max­Va­lue
    Int32 Min­Va­lue
    System.String ToS­t­ring(System.IFor­mat­P­ro­vi­der)
    System.TypeCode Get­T­y­pe­Co­de()
    System.String ToS­t­ring(System.String, System.IFor­mat­P­ro­vi­der)
    Int32 Com­pa­re­To(System.Obj­ect)
    Int32 Get­Has­h­Co­de()
    Boolean Equ­als(System.Obj­ect)
    System.String ToS­t­ring()
    System.String ToS­t­ring(System.String)
    Int32 Par­se(System.String)
    Int32 Par­se(System.String, System.Glo­ba­li­za­ti­on.Num­ber­S­t­y­les)
    Int32 Par­se(System.String, System.IFor­mat­P­ro­vi­der)
    Int32 Par­se(System.String, System.Glo­ba­li­za­ti­on.Num­ber­S­t­y­les, System.IFor­mat­P­ro­vi­der)
    System.Type Get­T­y­pe()
    * System.Ref­lec­ti­on.Mem­be­rIn­fo[] Mem­bers =type.Get­Mem­bers() - воз­в­ра­ща­ет всех чле­нов ти­па.
    * System.Ref­lec­ti­on.Met­ho­dIn­fo[] Met­hods =Type.Get­Met­hods() - воз­в­ра­ща­ет толь­ко ме­то­ды ти­па.
    * System.Ref­lec­ti­on.Fi­el­dIn­fo[] Fi­elds =Type.Get­Fi­elds() - воз­в­ра­ща­ет толь­ко по­ля ти­па.
    * System.Ref­lec­ti­on.Pro­per­t­yIn­fo[] Pro­per­ti­es = type.Get­P­ro­per­ti­es () - воз­в­ра­ща­ет толь­ко свой­ст­ва.
    * System.Ref­lec­ti­on.Even­tIn­fo[] Events = type.Ge­tE­vents() - воз­в­ра­ща­ет со­бы­тия ти­па.
    * System.Ref­lec­ti­on.Con­s­t­ruc­to­rIn­fo[] Con­s­t­ruc­tors = type.Get­Con­s­t­ruc­tors () - воз­в­ра­ща­ет кон­с­т­рук­то­ры ти­па.
    * System.Type[] In­ter­fa­ces = type.Ge­tIn­ter­fa­ces() - воз­в­ра­ща­ет ин­тер­фей­сы ти­па.
    
    Динамический вы­зов, ис­поль­зуя Type.Invo­ke­Mem­ber()
    Рассмотрим, как ди­на­ми­чес­ки выз­вать ме­тод, ис­поль­зуя type.Invo­ke­Mem­ber(). type.Invo­ke­Mem­ber() поз­во­ля­ет вы­зы­вать ме­то­ды по их наз­ва­ни­ям.
    
    Аргументы In­vo­ke­Mem­ber():
    1. Имя вы­зы­ва­емо­го ме­то­да. Пе­ре­да­ет­ся стро­кой.
    2. Чле­ны пе­ре­чис­ле­ния Bin­din­g­F­lags. Пе­ре­чис­ле­ние Bin­din­g­F­lags оп­ре­де­ля­ет фла­ги, ко­то­рые кон­т­ро­ли­ру­ют свя­зы­ва­ние и спо­соб по­ис­ка ти­пов и чле­нов.Нап­ри­мер, ис­кать ли сре­ди sta­tic ме­то­дов и т.п.
    3. Объ­ект Bin­der, оп­ре­де­ля­ющий свой­ст­ва и про­цесс свя­зы­ва­ния. Мо­жет быть null, для ис­поль­зо­ва­ния Bin­der по умол­ча­нию. Этот па­ра­метр поз­во­ля­ет поль­зо­ва­те­лю по­лу­чить внеш­ний кон­т­роль над спо­со­бом вы­бо­ра пе­ре­оп­ре­де­лен­ных фун­к­ций и ме­то­да­ми пре­об­ра­зо­ва­ния ар­гу­мен­тов.
    4. Объ­ект, для ко­то­ро­го на­до выз­вать ме­тод.
    5. Мас­сив ар­гу­мен­тов, пе­ре­да­ва­емых ме­то­ду.
    
    using System;
    using System.Ref­lec­ti­on ;
    namespace Ref­lec­ti­on­De­moC­S­harp
    {
    class Ref­lec­ted­T­y­pes
    {
    [STAThread]
    static vo­id Ma­in(string[] args)
    {
    Type Type­To­Ref­lect = Type.Get­T­y­pe("System.String");
    object re­sult = null;
    object[] ar­gu­ments = {"abc","xyz"};
    result = Type­To­Ref­lect.Invo­ke­Mem­ber ("Equ­als",
    BindingFlags.InvokeMethod, null, re­sult, ar­gu­ments);
    Console.WriteLine (re­sult.ToS­t­ring ());
    Console.ReadLine ();
    }
    }
    }
    Вывод бу­дет "fal­se".
    
    Дальше в статье рас­ска­зы­ва­ет­ся о тех­но­ло­гии Ref­lec­ti­on.Emit, ко­то­рая поз­во­ля­ет соз­да­вать as­sembly на ле­ту... Чес­т­но го­во­ря, я этой тех­но­ло­гии ви­жу толь­ко од­но при­ме­не­ние - соз­да­ние соб­с­т­вен­ных ком­пи­ля­то­ров. Впро­чем, сей­час мод­но встав­лять в прог­рам­мы воз­мож­ность до­пи­сы­ва­ния сво­их пла­ги­нов пря­мо из ок­на прог­рам­мы... а пос­коль­ку пла­ги­ны про­ще пи­сать на внут­рен­нем язы­ке при­ло­же­ния, то на­вер­ное и соб­с­т­вен­ный ком­пи­ля­тор мо­жет быть по­ле­зен. Хо­тя не знаю, есть же вся­кие скрип­то­вые ком­пи­ля­то­ры, уже вклю­чен­ные в сос­тав .net, есть тех­но­ло­гия VBA, ко­то­рую мно­гие не лю­бят, но ху­же она от это­го не ста­но­вить­ся и т.д.
    
Глава 4. Самодельные элементы управления 1.
    
    Элемент уп­рав­ле­ния - это лю­бой ку­сок ин­тер­фей­са, ко­то­рый са­жа­ет­ся в ок­но и как-то ре­аги­ру­ет на поль­зо­ва­те­ля. Вся­кие кноп­ки, ме­нюш­ки, спис­ки и про­чее.
    Часто бы­ва­ет си­ту­ация, ког­да хо­чет­ся (или тре­бу­ет­ся) как-то рас­ши­рить воз­мож­нос­ти эле­мен­та. Нап­ри­мер, хо­чет­ся сде­лать дру­гую ри­сов­ку пун­к­тов ме­ню, или вмес­то од­но­го пол­зун­ка нуж­но сде­лать два - для ус­та­нов­ки ми­ни­му­ма, мак­си­му­ма.
    В слу­ча­ях, ког­да на­до из­ме­нить ри­сов­ку, сна­ча­ла нуж­но пос­мот­реть - нель­зя ли это сде­лать с по­мощью род­ных ме­то­дов эле­мен­та. Нап­ри­мер, все пун­к­ты ме­ню, вы­па­да­ющих спис­ков и по­доб­ные им име­ют со­бы­тие Dra­wI­tem, в ко­то­ром мож­но опи­сы­вать свою фун­к­цию ри­со­ва­ния объ­ек­та.
    Рассмотрим на при­ме­ре. Нам на­до сде­лать вы­па­да­ющее ок­но, каж­дый пункт спис­ка ко­то­ро­го сос­то­ит из чис­ла, тек­с­та и цве­та.
    Класс хра­не­ния дан­ных:
    
    public class Com­bo­Da­ta
    {
    public int num­ber; // хра­ни­мое чис­ло
    public string text; // текст
    public Co­lor co­lor; // и цвет
    public Com­bo­Da­ta() { // пус­той кон­с­т­рук­тор
    number = 0;
    text = "";
    color = Co­lor.Black;
    }
    public Com­bo­Da­ta(int in­Num­ber, string in­Text, Co­lor in­Co­lor) { // па­ра­мет­ри­чес­кий кон­с­т­рук­тор
    number = in­Num­ber;
    text = in­Text;
    color = in­Co­lor;
    }
    public over­ri­de string ToS­t­ring() { // ме­тод для вы­во­да в стан­дар­т­ное вы­па­да­ющее ок­но
    return String.For­mat("{0}, {1}, {2}", num­ber, text, co­lor);
    }
    }
    
    Начальные дан­ные:
    
    public Form1() {
    InitializeComponent();
    items = new Com­bo­Da­ta[5];
    items[0] = new Com­bo­Da­ta(0, "text1", Co­lor.Red);
    items[1] = new Com­bo­Da­ta(1, "text2", Co­lor.Blue);
    items[2] = new Com­bo­Da­ta(2, "text3", Co­lor.Gre­en);
    items[3] = new Com­bo­Da­ta(3, "text4", Co­lor.Ma­gen­ta);
    items[4] = new Com­bo­Da­ta(4, "text5", Co­lor.Fro­mArgb(00, 221, 238));
    comboBox1.Items.AddRange(items);
    }
    public Com­bo­Da­ta[] items;
    
    В об­щем-то, мож­но и так ос­та­вить - бу­дет в вы­па­да­ющем ок­не та­кая кар­ти­на:
    
    Но как-то оно не пра­виль­но по­ка­зы­вать цвет циф­ра­ми. По­это­му, за­да­ем com­bo­Box свой­cт­во Draw­Mo­de = Ow­ner­D­raw­Fi­xed и про­пи­сы­ва­ем со­бы­тие Dra­wI­tem:
    
    private vo­id com­bo­Box1_Dra­wI­tem(obj­ect sen­der, Dra­wI­te­mE­ven­tArgs e) {
    SolidBrush br = new So­lid­B­rush(items[e.Index].co­lor); // соз­да­ем кисть цве­та эле­мен­та, ко­то­рый ри­су­ем
    if (e.Sta­te == Dra­wI­tem­S­ta­te.Se­lec­ted) { // ес­ли эле­мент выб­ран
    e.Graphics.FillRectangle(br, e.Bo­unds); // зак­ра­ши­ва­ем его на­шим цве­том
    e.Graphics.DrawString(items[e.Index].ToString(), e.Font, System­B­rus­hes.Hig­h­lig­h­t­Text, e.Bo­un­ds.Lo­ca­ti­on); // по­верх ри­су­ем текст
    e.DrawFocusRectangle();
    }
    else { // ес­ли эле­мент не выб­ран
    e.DrawBackground(); // ри­су­ем фон
    e.Graphics.DrawString(items[e.Index].ToString(), e.Font, System­B­rus­hes.Me­nu­Text, e.Bo­un­ds.Lo­ca­ti­on); // пи­шем стро­ку
    }
    br.Dispose(); // ос­во­бож­да­ем кисть
    }
        Теперь это выг­ля­дит так:
    
    Принцип, я ду­маю, по­ня­тен.
    
    Иногда бы­ва­ет на­до сде­лать на­бор групп эле­мен­тов. Мож­но пой­ти прос­тым пу­тем, и соз­да­вать каж­дый эле­мент в от­дель­нос­ти, а мож­но соз­дать эле­мент уп­рав­ле­ния Груп­па и в про­цес­се ра­бо­ты прог­рам­мы соз­да­вать его.
    Рассмотрим на том же при­ме­ре - на­до вы­вес­ти все зна­че­ния в ви­де тек­с­то­вых окон и па­не­лек цве­та. Ва­ри­ант 1:
    
    internal vo­id Cre­ate­Con­t­rols() { // фун­к­ция соз­да­ния эле­мен­тов уп­рав­ле­ния
    TextBox tb;
    Panel p;
    SuspendLayout();
    for (int i = 0; i ‹ items.Length; i++) {
    tb = new Tex­t­Box(); // тек­с­то­вое по­ле для чис­ла
    tb.Location = new Po­int(20, 50 + i * 25); // по­зи­ция
    tb.Size = new Si­ze(50, 20); // раз­мер
    tb.Text = items[i].num­ber.ToS­t­ring(); // текст
    Controls.Add(tb); // до­бав­ля­ем эле­мент в ок­но
    tb = new Tex­t­Box(); // тек­с­то­вое по­ле для тек­с­та
    tb.Location = new Po­int(75, 50 + i * 25);
    tb.Size = new Si­ze(50, 20);
    tb.Text = items[i].text;
    Controls.Add(tb);
    p = new Pa­nel(); // па­нель для цве­та
    p.Location = new Po­int(130, 50 + i * 25);
    p.Size = new Si­ze(50, 20);
    p.BackColor = items[i].co­lor; // за­да­ем цвет
    p.Tag = i; // ус­та­нав­ли­ва­ем иден­ти­фи­ка­тор
    p.Click += new Even­t­Han­d­ler(p_Click); // ус­та­нав­ли­ва­ем со­бы­тие клик
    Controls.Add(p);
    }
    ResumeLayout(false);
    }
    Добавим в Form1() пос­лед­ней стро­кой за­пуск фун­к­ции соз­да­ния эле­мен­тов
    
    CreateControls();
    И про­пи­шем со­бы­тие клик для па­не­лей­:
    
    void p_Click(obj­ect sen­der, Even­tArgs e) {
    int idx = (int)((Pa­nel)sen­der).Tag; // но­мер па­не­ли
    MessageBox.Show(String.Format("color pa­nel of item {0} just has be­en clic­ked", idx)); // со­об­ще­ние
    }
       Вот так вот.
    
     Вроде все в по­ряд­ке. С тем же ус­пе­хом мож­но сде­лать свой эле­мент уп­рав­ле­ния, ко­то­рый бу­дет сос­то­ять из двух тек­с­то­вых по­лей и од­ной па­нель­ки, и в фор­му до­бав­лять его. Соз­да­ем User­Con­t­rol, соз­да­ем на нем два тек­с­то­вых по­ля и па­нель­ку.
    Пишем два кон­с­т­рук­то­ра:
    
    public User­Con­t­rol1(Com­bo­Da­ta item) { // прос­той кон­с­т­рук­тор
    InitializeComponent();
    Num = item.num­ber;
    Tex = item.text;
    Col = item.co­lor;
    }
    
    public User­Con­t­rol1(Com­bo­Da­ta item, int pa­ne­lIdx, Even­t­Han­d­ler pa­nel­C­lick) { // кон­с­т­рут­кор с под­дер­ж­кой кли­ка
    InitializeComponent();
    Num = item.num­ber;
    Tex = item.text;
    Col = item.co­lor;
    panel1.Tag = pa­ne­lIdx;
    panel1.Click += pa­nel­C­lick;
    }
    
    Прописываем по­ля дан­ных:
    
    private int num; // чис­ло
    public int Num { // свой­ст­во чис­ло
    get { re­turn num; }
    set {
    num = va­lue;
    textBox1.Text = num.ToS­t­ring(); // об­нов­ле­ние тек­с­то­во­го по­ля
    }
    }
    private string tex; // текст
    public string Tex { // свой­ст­во текст
    get { re­turn tex; }
    set {
    tex = va­lue;
    textBox2.Text = tex;
    }
    }
    private Co­lor col; // цвет
    public Co­lor Col { // свой­ст­во цвет
    get { re­turn col; }
    set {
    col = va­lue;
    panel1.BackColor = col; // об­нов­ле­ние цве­та
    }
    }
    
    В Form1 пи­шем фун­к­цию соз­да­ния на­ше­го эле­мен­та:
    
    internal vo­id Cre­ate­User­Con­t­rols() {
    UserControl1 uc;
    SuspendLayout();
    for (int i = 0; i ‹ items.Length; i++) {
    uc = new User­Con­t­rol1(items[i], i, new Even­t­Han­d­ler(p_Click));
    uc.Location = new Po­int(20, 50 + i * 25);
    uc.Tag = i;
    Controls.Add(uc);
    }
    ResumeLayout(false);
    }
    
    И в Form1() ме­ня­ем Cre­ate­Con­t­rols() на Cre­ate­User­Con­t­rols().
    
    Все ра­бо­та­ет так же, но ис­поль­зу­ет са­мо­дель­ную груп­пу уп­рав­ле­ния. Удоб­с­т­во это­го под­хо­да в двух мо­мен­тах:
    1. Ес­ли вам по­на­до­бят­ся вся­кие фи­чи с вы­де­ле­ни­ем - в ва­шем рас­по­ря­же­нии со­бы­тие Pa­int для все­го эле­мен­та уп­рав­ле­ния, ра­моч­ку там сде­лать или еще че­го. При соз­да­нии каж­до­го по­ля в от­дель­нос­ти по­доб­ные ра­моч­ки бу­дут де­лать­ся слож­нее, да и ско­рость упа­дет за­мет­нее.
    2. Ес­ли та­ких эле­мен­тов уп­рав­ле­ния не 5, а 50, то ско­рость ра­бо­ты су­щес­т­вен­но по­вы­ша­ет­ся. Сис­те­ма про­ве­ря­ет ви­ди­мость эле­мен­та уп­рав­ле­ния, и ес­ли он це­ли­ком не­ви­ден - она не про­ве­ря­ет его до­чер­ние эле­мен­ты. Ста­ло быть, ес­ли ис­поль­зо­вать та­кой ме­тод груп­пи­ров­ки - сис­те­ма бу­дет про­ве­рять 50 эле­мен­тов на ви­ди­мость. А ес­ли по­ля при­пи­сы­вать фор­ме нап­ря­мую, как в ва­ри­ан­те 1, то сис­те­ма бу­дет про­ве­рять 150 эле­мен­тов.
    
    Ну и на­пос­ле­док: стан­дар­т­ный com­bo­Box име­ет со­бы­тие Se­lec­te­dIn­dex­C­han­ged, но он не со­об­ща­ет в нем, ка­кой эле­мент был выб­ран до сме­ны. Поп­ро­бу­ем мо­ди­фи­ци­ро­вать стан­дар­т­ный com­bo­Box так, что­бы он в этом со­бы­тии со­об­щал о пре­ды­ду­щем зна­че­нии.
    Создаем оче­ред­ной User­Con­t­rol и пе­ре­хо­дим в код, не об­ра­щая вни­ма­ния на ди­зай­нер. Ста­вим ро­ди­те­лем на­ше­го эле­мен­та класс Com­bo­Box вмес­то User­Con­t­rol:
    
    public par­ti­al class User­Con­t­rol3 : System.Win­dows.For­ms.Com­bo­Box
    
    Прописываем пе­ре­мен­ную для хра­не­ния пре­ды­ду­ще­го зна­че­ния выб­ран­но­го ин­дек­са:
    
    private int se­lec­te­dIn­dex_prev;
    
    В кон­с­т­рук­тор до­бав­ля­ем ини­ци­али­за­цию этой пе­ре­мен­ной­:
    
    selectedIndex_prev = -1;
    
    И пе­ре­пи­сы­ва­ем со­бы­тие Se­lec­te­dIn­dex­C­han­ged:
    
    protected over­ri­de vo­id On­Se­lec­te­dIn­dex­C­han­ged(Even­tArgs e) {
    base.OnSelectedIndexChanged(new User­Con­t­rol2Index­C­han­ge­dE­ven­tAr­gs(se­lec­te­dIn­dex_prev)); // стан­дар­т­ный об­ра­бот­чик, но с на­шим клас­сом ар­гу­мен­тов со­бы­тия
    selectedIndex_prev = this.Se­lec­te­dIn­dex; // из­ме­нить пред. ин­декс
    }
    
    Создаем от­дель­ный класс для ар­гу­мен­тов на­ше­го со­бы­тия:
    
    public class User­Con­t­rol2Index­C­han­ge­dE­ven­tArgs : Even­tArgs
    {
    public User­Con­t­rol2Index­C­han­ge­dE­ven­tAr­gs(int pre­vIdx) {
    prevSelectedIndex = pre­vIdx;
    }
    public int prev­Se­lec­te­dIn­dex;
    }
    
    Вот и все. Для про­вер­ки до­ба­вим в Form1 наш ви­до­из­ме­нен­ный com­bo­Box и про­пи­шем со­бы­тие Se­lec­te­dIn­dex­C­han­ged:
    
    private vo­id user­Con­t­rol31_Se­lec­te­dIn­dex­C­han­ged(obj­ect sen­der, Even­tArgs e) {
    MessageBox.Show(String.Format("Selected in­dex was chan­ged from {0} to {1}", ((User­Con­t­rol2Index­C­han­ge­dE­ven­tAr­gs)e).prev­Se­lec­te­dIn­dex, user­Con­t­rol31.Se­lec­te­dIn­dex));
    }
    
    Не за­бы­ва­ем в кон­с­т­рук­тор фор­мы до­ба­вить за­пол­не­ние на­ше­го com­bo­Box'a зна­че­ни­ями:
    
    userControl31.Items.AddRange(items);
    Каталог про­ек­та для MS Vi­su­al Stu­dio 2005 с при­ме­ром на­хо­дит­ся в ар­хи­ве при­ме­ров, под­ка­та­лог user­con­t­rol1.
    Архив при­ме­ров мож­но ска­чать здесь: http://www.robinland.com/csharp-basis/samples.zip.
    
Глава 5. Самодельные элементы управления 2.
    В предыдущей главе мы рас­смот­ре­ли си­ту­ации при ко­то­рых мож­но ком­би­ни­ро­вать уже су­щес­т­ву­ющие эле­мен­ты уп­рав­ле­ния, или дос­та­точ­но прос­то мо­ди­фи­ци­ро­вать их. Од­на­ко, бы­ва­ют си­ту­ации ког­да ни­ка­кой мо­ди­фи­ка­ци­ей нуж­ный ре­зуль­тат не бу­дет дос­тиг­нут. Рас­смот­рим од­ну та­кую на при­ме­ре.
    Необходимо сде­лать эле­мент уп­рав­ле­ния, поз­во­ля­ющий ус­та­нав­ли­вать два зна­че­ния в за­дан­ном ин­тер­ва­ле. Для ус­та­нов­ки од­но­го зна­че­ния есть род­ной эле­мент уп­рав­ле­ния - sli­der (или Trac­k­Bar). Ле­ни­вые лю­ди ска­жут, что не­че­го тут из­в­ра­щать­ся, дос­та­точ­но по­са­дить два слай­де­ра и не мо­ро­чить го­ло­ву. :) С дру­гой сто­ро­ны, сде­лать свой эле­мент уп­рав­ле­ния дос­та­точ­но прос­то, а ин­тер­фей­с он мо­жет зна­чи­тель­но улуч­шить, ибо чем мень­ше эле­мен­тов в ок­не - тем луч­ше.
    
    Приступим.
    К про­ек­ту при­со­еди­ня­ем User­Con­t­rol и за­да­ем ос­нов­ные свой­ст­ва:
    
    this.MaximumSize = new System.Dra­wing.Si­ze(3000, 15); // мак­си­маль­ный раз­мер
    this.MinimumSize = new System.Dra­wing.Si­ze(50, 15); // ми­ни­маль­ный раз­мер
    this.Size = new System.Dra­wing.Si­ze(50, 15); // стар­то­вый раз­мер
    
    Наш эле­мент уп­рав­ле­ния, с точ­ки зре­ния ри­со­ва­ния, сос­то­ит из вдав­лен­ной по­лос­ки и двух пол­зун­ков. Каж­дый пол­зу­нок мо­жет быть в трех сос­то­яни­ях - нор­маль­ном, под­с­ве­чен­ном (ког­да мыш­ка над ним) и ак­тив­ном (ког­да его та­щат). за­да­дим со­от­вет­с­т­ву­ющие фла­ги. А за­од­но соз­да­дим пря­мо­уголь­ни­ки пол­зун­ков, что­бы про­ще бы­ло по­том ри­со­вать и от­с­ле­жи­вать:
    
    private bo­ol sli­der1drag; // пол­зу­нок 1 та­щат
    private bo­ol sli­der2drag; // пол­зу­нок 2 та­щат
    private bo­ol sli­der1ho­ver; // пол­зу­нок 1 под­с­ве­чен
    private bo­ol sli­der2ho­ver; // пол­зу­нок 2 под­с­ве­чен
    private Rec­tan­g­le sli­der1; // пол­зу­нок 1
    private Rec­tan­g­le sli­der2; // пол­зу­нок 2
    
    Теперь мож­но поп­ро­бо­вать на­ри­со­вать. По су­ти мы бу­дем ри­со­вать три объ­ем­ных пря­мо­уголь­ни­ка, один утоп­лен­ный­, два вы­пук­лых... соз­да­дим от­дель­ную фун­к­цию для это­го:
    
    private vo­id Draw­Rec­tan­g­le3D(Grap­hics gr, int x, int y, int width, int he­ight, bo­ol pus­hed) {
    if (pus­hed) {
    gr.DrawLine(SystemPens.ControlLightLight, x + 1, y + he­ight, x + width, y + he­ight);
    gr.DrawLine(SystemPens.ControlLightLight, x + width, y + he­ight, x + width, y + 1);
    gr.DrawLine(SystemPens.ControlDarkDark, x, y, x + width, y);
    gr.DrawLine(SystemPens.ControlDarkDark, x, y, x, y + he­ight);
    }
    else {
    gr.DrawLine(SystemPens.ControlDarkDark, x + 1, y + he­ight, x + width, y + he­ight);
    gr.DrawLine(SystemPens.ControlDarkDark, x + width, y + he­ight, x + width, y + 1);
    gr.DrawLine(SystemPens.ControlLightLight, x, y, x + width, y);
    gr.DrawLine(SystemPens.ControlLightLight, x, y, x, y + he­ight);
    }
    }
    Рисуем сис­тем­ны­ми цве­та­ми, что­бы не вы­би­вать­ся из ус­та­нов­лен­но­го в сис­те­ме сти­ля офор­м­ле­ния, хо­тя бы по цве­ту.
    И ва­ри­ант фун­к­ции для ра­бо­ты с объ­ек­том Rec­tan­g­le:
    
    private vo­id Draw­Rec­tan­g­le3D(Grap­hics gr, Rec­tan­g­le rect, bo­ol pus­hed) {
    if (pus­hed) {
    gr.DrawLine(SystemPens.ControlLightLight, rect.X + 1, rect.Bot­tom, rect.Right, rect.Bot­tom);
    gr.DrawLine(SystemPens.ControlLightLight, rect.Right, rect.Bot­tom, rect.Right, rect.Y + 1);
    gr.DrawLine(SystemPens.ControlDarkDark, rect.X, rect.Y, rect.Right, rect.Y);
    gr.DrawLine(SystemPens.ControlDarkDark, rect.X, rect.Y, rect.X, rect.Bot­tom);
    }
    else {
    gr.DrawLine(SystemPens.ControlDarkDark, rect.X + 1, rect.Bot­tom, rect.Right, rect.Bot­tom);
    gr.DrawLine(SystemPens.ControlDarkDark, rect.Right, rect.Bot­tom, rect.Right, rect.Y + 1);
    gr.DrawLine(SystemPens.ControlLightLight, rect.X, rect.Y, rect.Right, rect.Y);
    gr.DrawLine(SystemPens.ControlLightLight, rect.X, rect.Y, rect.X, rect.Bot­tom);
    }
    }
    Теперь соз­да­ем об­ра­бот­чик со­бы­тие Pa­int и опи­сы­ва­ем его:
    
    private vo­id User­Con­t­rol1_Pa­int(obj­ect sen­der, Pa­in­tE­ven­tArgs e) {
    DrawRectangle3D(e.Graphics, 2, He­ight / 2 - 2, Width - 4, 4, true); // на­ри­со­вать утоп­лен­ную по­лос­ку
    if (e.Clip­Rec­tan­g­le.Inter­sec­t­s­With(sli­der1)) { // ес­ли об­нов­ля­емый ре­ги­он пе­ре­се­ка­ет пол­зу­нок 1
    if (sli­der1drag) { // ес­ли пол­зу­нок 1 та­щат
    e.Graphics.FillRectangle(SystemBrushes.ControlDark, sli­der1); // зак­ра­сить его тем­ным цве­том
    }
    else if (sli­der1ho­ver) { // ес­ли пол­зу­нок под­с­ве­чен
    e.Graphics.FillRectangle(SystemBrushes.ButtonHighlight, sli­der1); // зак­ра­сить его цве­том под­с­вет­ки
    }
    else { // ес­ли прос­то на­ри­со­вать
    e.Graphics.FillRectangle(SystemBrushes.Control, sli­der1); // зак­ра­сить сис­тем­ным цве­том эле­мен­та уп­рав­ле­ния
    }
    DrawRectangle3D(e.Graphics, sli­der1, fal­se); // на­ри­со­вать объ­ем­ную вы­пук­лую рам­ку
    }
    if (e.Clip­Rec­tan­g­le.Inter­sec­t­s­With(sli­der2)) { // то же для пол­зун­ка 2
    if (sli­der2drag) {
    e.Graphics.FillRectangle(SystemBrushes.ControlDark, sli­der2);
    }
    else if (sli­der2ho­ver) {
    e.Graphics.FillRectangle(SystemBrushes.ButtonHighlight, sli­der2);
    }
    else {
    e.Graphics.FillRectangle(SystemBrushes.Control, sli­der2);
    }
    DrawRectangle3D(e.Graphics, sli­der2, fal­se);
    }
    }
    
    Теперь на­до за­дать пря­мо­уголь­ни­ки пол­зун­ков и все зна­че­ния на ко­то­рые они опи­ра­ют­ся. Сна­ча­ла не­об­хо­ди­мые зна­че­ния:
    Минимум по­лос­ки слай­де­ра:
    
    private int min­Val; // по­ле для внут­рен­не­го поль­зо­ва­ния
    [Browsable(true)] // по­ка­зы­вать в ди­зай­не­ре
    [RefreshProperties(RefreshProperties.All)] // об­нов­лять ос­таль­ные свой­ст­ва при из­ме­не­нии это­го
    public int Mi­ni­mum­Va­lue { // свой­ст­во (ви­ди­мое и в ди­зай­не­ре)
    get { re­turn min­Val; }
    set {
    if (va­lue ›= max­Val) { throw new Ar­gu­men­tEx­cep­ti­on("mi­ni­mum va­lue must be ‹ ma­xi­mum"); } // про­ве­рить на мень­ше мак­си­му­ма
    minVal = va­lue; // ус­та­но­вить зна­че­ние
    if (Va­lue2 ‹= min­Val) { Va­lue2 = min­Val+1; } // поп­ра­вить зна­че­ния пол­зун­ков ес­ли на­до
    if (Va­lue1 ‹ min­Val) { Va­lue1 = min­Val; }
    SetPos1(); // поп­ра­вить пря­мо­уголь­ник пол­зун­ка 1
    SetPos2();// поп­ра­вить пря­мо­уголь­ник пол­зун­ка 2
    }
    }
    Аналогично ос­таль­ные по­ля:
    
    private int max­Val; // мак­си­мум по­лос­ки слай­де­ра
    [Browsable(true)]
    [RefreshProperties(RefreshProperties.All)]
    public int Ma­xi­mum­Va­lue {
    get { re­turn max­Val; }
    set {
    if (va­lue ‹= min­Val) { throw new Ar­gu­men­tEx­cep­ti­on("ma­xi­mum va­lue must be › mi­ni­mum"); }
    maxVal = va­lue;
    if (Va­lue1 ›= max­Val) { Va­lue1 = max­Val-1; }
    if (Va­lue2 › max­Val) { Va­lue2 = max­Val; }
    SetPos1();
    SetPos2();
    }
    }
    private int val1; // зна­че­ние пол­зун­ка 1
    [Browsable(true)]
    public int Va­lue1 {
    get { re­turn val1; }
    set {
    if (va­lue › max­Val) { throw new Ar­gu­men­tEx­cep­ti­on("va­lue1 must be bet­we­en min and max va­lu­es"); } // мень­ше мак­си­му­ма
    if (va­lue ‹ min­Val) { throw new Ar­gu­men­tEx­cep­ti­on("va­lue1 must be bet­we­en min and max va­lu­es"); } // боль­ше ми­ни­му­ма
    if (va­lue ›= Va­lue2) { throw new Ar­gu­men­tEx­cep­ti­on("va­lue1 must be ‹ Va­lue2"); } // мень­ше пол­зун­ка 2
    val1 = va­lue; // ус­та­но­вить зна­че­ние
    SetPos1(); // поп­ра­вить пря­мо­уголь­ник пол­зун­ка 1
    }
    }
    private int val2; // ана­ло­гич­но пол­зу­нок 2
    [Browsable(true)]
    public int Va­lue2 {
    get { re­turn val2; }
    set {
    if (va­lue › max­Val) { throw new Ar­gu­men­tEx­cep­ti­on("va­lue2 must be bet­we­en min and max va­lu­es"); }
    if (va­lue ‹ min­Val) { throw new Ar­gu­men­tEx­cep­ti­on("va­lue2 must be bet­we­en min and max va­lu­es"); }
    if (va­lue ‹= Va­lue1) { throw new Ar­gu­men­tEx­cep­ti­on("va­lue2 must be › Va­lue1"); }
    OnValueChanged(new User­Con­t­rol1Va­lu­eC­han­ge­dE­ven­tAr­gs(val2, va­lue, 2));
    val2 = va­lue;
    SetPos2();
    }
    }
    
    Теперь за­да­дим фун­к­ции оп­ре­де­ле­ния пря­мо­уголь­ни­ков пол­зун­ков:
    
    private vo­id Set­Pos1() {
    slider1.X = (int)(((do­ub­le)val1 - min­Val) / (max­Val - min­Val) * (Width - 5));
    }
    private vo­id Set­Pos2() {
    slider2.X = (int)(((do­ub­le)val2 - min­Val) / (max­Val - min­Val) * (Width - 5));
    }
    
    И до­ба­вим ини­ци­али­за­цию в кон­с­т­рук­тор:
    
    public User­Con­t­rol1() {
    InitializeComponent();
    minVal = 0;
    maxVal = 100;
    val2 = 100;
    slider1 = new Rec­tan­g­le(0, 2, 4, He­ight - 4);
    slider2 = new Rec­tan­g­le(0, 2, 4, He­ight - 4);
    SetPos1();
    SetPos2();
    }
    
    Поскольку по­ло­же­ние пол­зун­ков опи­ра­ет­ся еще и на раз­мер эле­мен­та уп­рав­ле­ния, до­ба­вим со­бы­тие Si­zeC­han­ged и опи­шем его:
    
    private vo­id User­Con­t­rol1_Si­zeC­han­ged(obj­ect sen­der, Even­tArgs e) {
    SetPos1();
    SetPos2();
    }
    
    Приступаем к "мы­ши­ным" фун­к­ци­ям:
    Создаем со­бы­тия на­жа­тие мыш­ки, от­пус­ка­ние мыш­ки и дви­же­ние мыш­ки.
    Нажатие мыш­ки:
    
    private vo­id User­Con­t­rol1_Mo­use­Down(obj­ect sen­der, Mo­use­Even­tArgs e) {
    if (sli­der1.Con­ta­ins(e.Lo­ca­ti­on)) { // ес­ли на­жа­ли на пол­зу­нок 1
    slider1drag = true; // ус­та­но­вить флаг пол­зу­нок 1 та­щат
    Invalidate(slider1); // об­но­вить ри­сов­ку пол­зун­ка 1
    }
    else if (sli­der2.Con­ta­ins(e.Lo­ca­ti­on)) { // то же для пол­зун­ка 2
    slider2drag = true;
    Invalidate(slider2);
    }
    }
    
    Отпускание мыш­ки:
    
    private vo­id User­Con­t­rol1_Mo­use­Up(obj­ect sen­der, Mo­use­Even­tArgs e) {
    if (sli­der1drag) { // ес­ли та­щи­ли пол­зу­нок 1
    slider1drag = fal­se; // снять вы­де­ле­ние
    slider1hover = fal­se;
    Invalidate(slider1); // пе­ре­ри­со­вать внут­рен­нюю часть
    }
    else if (sli­der2drag) { // то же для пол­зун­ка 2
    slider2drag = fal­se;
    slider2hover = fal­se;
    Invalidate(slider2);
    }
    }
    
    Перемещение мыш­ки. Тут нам по­на­до­бит­ся воз­мож­ность об­нов­лять не толь­ко сам пол­зу­нок, но всю об­ласть от пре­ды­ду­ще­го по­ло­же­ния пол­зун­ка, до его те­ку­ще­го по­ло­же­ния. Ина­че бу­дут ос­та­вать­ся по­лос­ки при быс­т­ром дви­же­нии. Для это­го соз­да­дим еще од­но по­ле Rec­tan­g­le, и про­пи­шем его ини­ци­али­за­цию в кон­с­т­рук­тор:
    
    private Rec­tan­g­le in­va­li­da­te­Rect;
    public User­Con­t­rol1() {
    ...
    invalidateRect = new Rec­tan­g­le();
    }
    
    Вот те­перь фун­к­ция пе­ре­ме­ще­ния мыш­ки:
    
    private vo­id User­Con­t­rol1_Mo­use­Mo­ve(obj­ect sen­der, Mo­use­Even­tArgs e) {
    if (sli­der1drag) { // ес­ли пол­зу­нок 1 та­щат
    if (e.X ›= 2 e.X ‹= Width - 3 e.X ‹ sli­der2.Left+1) { // про­ве­рить мож­но ли сю­да дви­гать­ся
    invalidateRect = sli­der1; // за­пом­ним ста­рое по­ло­же­ние пол­зун­ка
    Value1 = (int)Math.Ro­und(min­Val + ((do­ub­le)(e.X - 2) / (Width - 5)) * (max­Val - min­Val)); // ус­та­но­вим но­вое по­ло­же­ние по мыш­ке, что об­но­вит по­ло­же­ние пол­зун­ка
    invalidateRect = Rec­tan­g­le.Uni­on(sli­der1, in­va­li­da­te­Rect); // по­лу­чим пря­мо­уголь­ник объ­еди­ня­ющий ста­рое и но­вое по­ло­же­ние
    Invalidate(Rectangle.Inflate(invalidateRect, 1, 1)); // об­но­вим пря­мо­уголь­ник и при­ле­га­ющие пик­се­ли
    }
    }
    else if (sli­der2drag) { // то же для пол­зун­ка 2
    if (e.X ›= 2 e.X ‹= Width - 3 e.X › sli­der1.Right-1) {
    invalidateRect = sli­der2;
    Value2 = (int)Math.Ro­und(min­Val + ((do­ub­le)(e.X - 2) / (Width - 5)) * (max­Val - min­Val));
    invalidateRect = Rec­tan­g­le.Uni­on(sli­der2, in­va­li­da­te­Rect);
    Invalidate(Rectangle.Inflate(invalidateRect, 1, 1));
    }
    }
    else { // ес­ли ни­ко­го не та­щат
    if (sli­der1.Con­ta­ins(e.Lo­ca­ti­on)) { // ес­ли мыш­ка над пер­вым пол­зун­ком
    if (!sli­der1ho­ver) { // ес­ли флаг под­с­вет­ки не ус­та­нов­лен
    slider1hover = true; // ус­та­но­вить
    Invalidate(slider1); // пе­ре­ри­со­вать внут­рен­нюю часть пол­зун­ка
    }
    }
    else if (sli­der2.Con­ta­ins(e.Lo­ca­ti­on)) { // то же для пол­зун­ка 2
    //draw ho­ver sta­te
    if (!sli­der2ho­ver) {
    slider2hover = true;
    Invalidate(slider2);
    }
    }
    else { // ес­ли мыш­ка не над пол­зун­ка­ми
    if (sli­der1ho­ver) { // ес­ли пол­зу­нок 1 был под­с­ве­чен
    slider1hover = fal­se; // снять под­с­вет­ку
    Invalidate(slider1); // пе­ре­ри­со­вать внут­рен­нюю часть
    }
    else if (sli­der2ho­ver) { // то же для пол­зун­ка 2
    slider2hover = fal­se;
    Invalidate(slider2);
    }
    }
    }
    GC.Collect(); // очис­тить па­мять
    }
    
    Очистка па­мя­ти нуж­на, по­то­му как фун­к­ции Rec­tan­g­le.Uni­on и Rec­tan­g­le.Infla­te соз­да­ют но­вые объ­ек­ты Rec­tan­g­le, ко­то­рые пос­ле окон­ча­ния фун­к­ции ухо­дят в му­сор.
    
    Добавим еще мо­ди­фи­ка­тор клас­са, что­бы его бы­ло нор­маль­но вид­но в ди­зай­не­ре:
    
    [DesignTimeVisible(true)]
    public par­ti­al class User­Con­t­rol1 : User­Con­t­rol
    {
    
    Вобщем-то эле­мент уп­рав­ле­ния ра­бо­та­ет. Мож­но соз­дать фор­му и по­са­дить на нее наш эле­мент, нас­т­ро­ить и пос­мот­реть как ра­бо­та­ет. Вот толь­ко не очень удоб­но - при из­ме­не­нии зна­че­ния, он об этом не со­об­ща­ет. До­ба­вим ему со­бы­тие из­ме­не­ние зна­че­ния:
    
    [Browsable(true)] // вид­но в ди­зай­не­ре
    public event Va­lu­eC­han­ge­dE­ven­t­De­le­ga­te Va­lu­eC­han­ged = null; // со­бы­тие
    public de­le­ga­te vo­id Va­lu­eC­han­ge­dE­ven­t­De­le­ga­te(obj­ect sen­der, User­Con­t­rol1Va­lu­eC­han­ge­dE­ven­tArgs e); // де­ле­гат об­ра­бот­чи­ка со­бы­тия
    protected vir­tu­al vo­id On­Va­lu­eC­han­ged(User­Con­t­rol1Va­lu­eC­han­ge­dE­ven­tArgs e) { // об­ра­бот­чик
    if (Va­lu­eC­han­ged != null) {
    ValueChanged(this, e);
    }
    }
    
    Нам по­на­до­бит­ся еще класс ар­гу­мен­тов со­бы­тия:
    
    public class User­Con­t­rol1Va­lu­eC­han­ge­dE­ven­tArgs : Even­tArgs // ар­гу­мен­ты со­бы­тия
    {
    public User­Con­t­rol1Va­lu­eC­han­ge­dE­ven­tAr­gs(int inOl­d­Va­lue, int in­New­Va­lue, byte in­Va­lue) { // кон­с­т­рук­тор
    oldValue = inOl­d­Va­lue; // ста­рое зна­че­ние
    newValue = in­New­Va­lue; // но­вое зна­че­ние
    value = in­Va­lue; // ка­кой пол­зу­нок
    }
    public int ol­d­Va­lue;
    public int new­Va­lue;
    /// ‹sum­mary›
    /// 1 - va­lue 1, 2 - va­lue 2
    /// ‹/sum­mary›
    public byte va­lue;
    }
    
    И вве­дем вы­зов со­бы­тия при из­ме­не­нии зна­че­ний­:
    public int Va­lue1 {
    get { re­turn val1; }
    set {
    if (va­lue › max­Val) { throw new Ar­gu­men­tEx­cep­ti­on("va­lue1 must be bet­we­en min and max va­lu­es"); }
    if (va­lue ‹ min­Val) { throw new Ar­gu­men­tEx­cep­ti­on("va­lue1 must be bet­we­en min and max va­lu­es"); }
    if (va­lue ›= Va­lue2) { throw new Ar­gu­men­tEx­cep­ti­on("va­lue1 must be ‹ Va­lue2"); }
    OnValueChanged(new User­Con­t­rol1Va­lu­eC­han­ge­dE­ven­tAr­gs(val1, va­lue, 1)); // вы­зов со­бы­тия
    val1 = va­lue;
    SetPos1();
    }
    }
    
    Аналогичную строч­ку до­ба­вим и для вто­ро­го зна­че­ния:
    OnValueChanged(new User­Con­t­rol1Va­lu­eC­han­ge­dE­ven­tAr­gs(val2, va­lue, 2)); // вы­зов со­бы­тия
    
    И пос­лед­ний штрих - ус­та­но­вим для клас­са со­бы­ти­ем по умол­ча­нию - на­ше со­бы­тие:
    [DefaultEvent("ValueChanged")]
    public par­ti­al class User­Con­t­rol1 : User­Con­t­rol
    
    Вот и все. Мож­но ис­поль­зо­вать и в ди­зай­не­ре и во вре­мя ра­бо­ты прог­рам­мы. Сде­ла­ем Form1, до­ба­вим в нее наш эле­мент уп­рав­ле­ния, сде­ла­ем па­ру тек­с­то­вых око­шек и про­пи­шем для на­ше­го эле­мен­та уп­рав­ле­ния со­бы­тие Va­lu­eC­han­ged:
    private vo­id user­Con­t­rol11_Va­lu­eC­han­ged(obj­ect sen­der, User­Con­t­rol1Va­lu­eC­han­ge­dE­ven­tArgs e) {
    if (e.va­lue == 1) { // ес­ли пер­вый пол­зу­нок
    textBox1.Text = e.new­Va­lue.ToS­t­ring(); // внес­ти но­вое зна­че­ние в тек­с­то­вое по­ле 1
    }
    else if (e.va­lue == 2) { // ана­ло­гич­но для пол­зун­ка 2
    textBox2.Text = e.new­Va­lue.ToS­t­ring();
    }
    }
    
    Да до­ба­вим в кон­с­т­рук­тор фор­мы та­кие строч­ки:
    textBox1.Text = user­Con­t­rol11.Va­lue1.ToS­t­ring(); // за­пи­сать на­чаль­ное зна­че­ние пол­зун­ка 1 в тек­с­то­вое по­ле 1
    textBox2.Text = user­Con­t­rol11.Va­lue2.ToS­t­ring();// ана­ло­гич­но для пол­зун­ка 2
    
    Вот все и ра­бо­та­ет. Не иде­аль­но, ко­неч­но. Но "де­ше­во, на­деж­но и прак­тич­но".
    Каталог про­ек­та для MS Vi­su­al Stu­dio 2005 с при­ме­ром на­хо­дит­ся в ар­хи­ве при­ме­ров, под­ка­та­лог user­con­t­rol2.
    Архив при­ме­ров мож­но ска­чать здесь: http://www.robinland.com/csharp-basis/samples.zip.
        
Часть 4.
    Основы GDI+.
Гла­ва 1. Ос­нов­ные по­ня­тия и ошиб­ки.
    
    
    
    Приступаю к рас­ска­зу как ри­со­вать в .NET.
    Несколько ввод­ных слов.
    Если вы хо­ти­те что-то изоб­ра­зить на эк­ра­не - вам на­до это что-то на­ри­со­вать. Дру­го­го спо­со­ба нет. Все ок­на, кноп­ки и про­чие кон­т­ро­лы - это все ри­су­ет­ся, прос­то код ри­со­ва­ния на­пи­сан за вас. Но те, кто соз­да­ют свои эле­мен­ты уп­рав­ле­ния зна­ют, что ри­со­вать при­хо­дит­ся все - на­чи­ная от рам­ки и за­кан­чи­вая вве­ден­ным тек­с­том.
    Есть раз­ные тех­но­ло­гии ри­со­ва­ния. Мож­но наз­вать три ос­нов­ные - GDI+, Di­rectX, сис­тем­ные фун­к­ции.
    Системные фун­к­ции име­ют очень ог­ра­ни­чен­ные воз­мож­нос­ти, од­на­ко ра­бо­та­ют очень быс­т­ро. В слу­чае тех же кон­т­ро­лов, луч­ше поль­зо­вать­ся фун­к­ци­ями сис­те­мы для ри­со­ва­ния кус­ков сти­ля, эти фун­к­ции ра­бо­та­ют нам­но­го быс­т­рее, чем GDI+, пос­коль­ку вмес­то дли­тель­но­го про­ри­со­вы­ва­ния кар­тин­ки, они ко­пи­ру­ют со­дер­жи­мое нап­ря­мую в ви­део па­мять.
    DirectX - са­мая быс­т­рая тех­но­ло­гия, пос­коль­ку ри­су­ет пря­мо в ви­део па­мя­ти, ис­поль­зуя гра­фи­чес­кие чи­пы-уско­ри­те­ли. Од­на­ко име­ет ряд не­дос­тат­ков - ко­ли­чес­т­во и объ­ем под­г­ру­жа­емых биб­ли­отек и драй­ве­ров мо­жет пре­вы­шать объ­ем прог­рам­мы в нес­коль­ко раз. Не го­во­ря уже о том, что прог­рам­ми­ро­вать под Di­rectX мно­го слож­нее, чем под GDI+.
    GDI+ - са­мая мед­лен­ная тех­но­ло­гия. И при этом са­мая удоб­ная для прог­рам­мис­та. В от­ли­чии от Di­rectX - ри­су­ет ис­поль­зуя ос­нов­ной про­цес­сор, по­это­му за­ни­ма­ет ку­чу ре­сур­сов и вре­ме­ни. В от­ли­чии от сис­тем­ных фун­к­ций име­ет ог­ром­ные воз­мож­нос­ти. Го­во­рят, что в бли­жай­шем бу­ду­щем (в рай­оне .NET 4.0) GDI+ то­же бу­дет ри­со­вать ис­поль­зую гра­фи­чес­кие чи­пы...
    
    Основные мо­мен­ты GDI+
    Все ри­со­ва­ние в GDI+ ве­дет­ся че­рез объ­ект клас­са Grap­hics. Это, мож­но ска­зать, яд­ро тех­но­ло­гии. Объ­ект со­дер­жит фун­к­ции ри­со­ва­ния плос­ких при­ми­ти­вов, изоб­ра­же­ний­, тек­с­та, под­дер­жи­ва­ет прос­т­ран­с­т­вен­ные пре­об­ра­зо­ва­ния, уме­ет про­во­дить сгла­жи­ва­ние в раз­ных ре­жи­мах и пр. Объ­ект Grap­hics мо­жет быть соз­дан от лю­бо­го кон­т­ро­ла, вклю­чая фор­му, от лю­бо­го объ­ек­та Ima­ge, и еще нес­коль­ки­ми спо­со­ба­ми, ко­то­рые вряд ли по­на­до­бят­ся.
    Итак, ес­ли вам на­до что-то где-то на­ри­со­вать, дей­ст­ву­ете так:
    1. Соз­да­ете объ­ект Grap­hics, или по­лу­ча­ете уже соз­дан­ный­, от то­го объ­ек­та, на ко­то­ром вам на­до ри­со­вать.
    2. Ри­су­ете че­рез объ­ект Grap­hics.
    3. Уда­ля­ете объ­ект Grap­hics.
    
    Graphics gr = Grap­hics.From­H­w­nd(pa­nel1.Han­d­le);
    gr.DrawLine(pen1, po­int1, po­int2);
    gr.FillEllipse(brush1, 0,0,100,100);
    gr.DrawEllipse(pen2, 0,0,100,100);
    gr.Dispose();
    
    
    Если ва­шей прог­рам­ме нуж­но очень мно­го ри­со­вать, да еще из раз­ных фун­к­ций­, вы мо­же­те сде­лать еди­ный объ­ект Grap­hics, и поль­зо­вать­ся им от раз­ных фун­к­ций. Впро­чем, это как пра­ви­ло пло­хая ме­то­ди­ка, го­раз­до луч­ше ис­поль­зо­вать род­ные event.
    
    Использование Grap­hics в event.
    Обычно это выг­ля­дит так:
    
    private vo­id pa­nel1_Pa­int(obj­ect sen­der, System.Win­dows.For­ms.Pa­in­tE­ven­tArgs e) {
    e.Graphics.FillEllipse(Brushes.Magenta,0,0,150,150);
    }
    
    И луч­ше все­го, весь код ри­со­ва­ния зак­ла­ды­вать в event. Вам ник­то не ме­ша­ет сде­лать его силь­но па­ра­мет­ри­чес­ким и пр. Мо­же­те в event пос­та­вить вы­зов фун­к­ции соб­с­т­вен­но ри­со­ва­ния, и пе­ре­да­вать объ­ект e.Grap­hics как ар­гу­мент, с мо­ди­фи­ка­то­ром ref. Это поз­во­лит вам ис­поль­зо­вать фун­к­цию ри­со­ва­ния не толь­ко для ри­со­ва­ния на эк­ра­не, но и для ри­со­ва­ния на прин­те­ре, ес­ли у вас бу­дет под­дер­ж­ка пе­ча­ти, для ри­со­ва­ния на Ima­ge, ес­ли вы бу­де­те сох­ра­нять изоб­ра­же­ние в файл, при­чем не тра­тя лиш­них ре­сур­сов на на­пи­са­ния трех оди­на­ко­вых фун­к­ций­, или на соз­да­ние но­вых объ­ек­тов Grap­hics и т.д.
    
    Основные при­емы
    Рисование ди­на­ми­чес­кой ин­фор­ма­ции в фор­ме, обыч­но, про­из­во­дит­ся дву­мя пу­тя­ми - ли­бо в кон­т­ро­ле pa­nel, ли­бо в Ima­ge кон­т­ро­ла pic­tu­re­Box. Ко­неч­но, вам ник­то не ме­ша­ет ри­со­вать пря­мо в фор­ме, ес­ли у вас вся фор­ма от­ве­де­на под ри­со­ва­ние, но там свои заморочки. Мы рас­c­мот­рим схе­му ри­со­ва­ния че­рез event Pa­int в двух кон­т­ро­лах.
    Общая схе­ма та­кая:
    1. Фун­к­ция pa­nel1_Pa­int или pic­tu­re­Box1_Pa­int со­дер­жит па­ра­мет­ри­чес­кий код ри­со­ва­ния.
    2. Ос­таль­ные кон­т­ро­лы ме­ня­ют па­ра­мет­ры ри­со­ва­ния.
    Предположим есть две ра­ди­ок­ноп­ки - од­на за­да­ет крас­ный цвет, дру­гая си­ний. Тог­да код ри­со­ва­ния бу­дет выг­ля­деть при­мер­но так:
    Для pa­nel:
    
    private vo­id pa­nel1_Pa­int(obj­ect sen­der, System.Win­dows.For­ms.Pa­in­tE­ven­tArgs e) {
    if (ra­di­oBut­ton1.Chec­ked) {
    e.Graphics.FillEllipse(Brushes.Red,0,0,150,150);
    }
    else {
    e.Graphics.FillEllipse(Brushes.Blue,0,0,150,150);
    }
    }
    
    Для pic­tu­re­Box:
    
    private vo­id pic­tu­re­Box1_Pa­int(obj­ect sen­der, System.Win­dows.For­ms.Pa­in­tE­ven­tArgs e) {
    Graphics gr = Grap­hics.Fro­mI­ma­ge(pic­tu­re­Box1.Ima­ge);
    gr.Clear(Color.White);
    if (ra­di­oBut­ton1.Chec­ked) {
    gr.FillEllipse(Brushes.Red,0,0,150,150);
    }
    else {
    gr.FillEllipse(Brushes.Blue,0,0,150,150);
    }
    gr.Dispose();
    }
    В пос­лед­нем слу­чае - gr.Cle­ar(Co­lor.Whi­te) - за­пол­ня­ет всю кар­тин­ку бе­лым цве­том, это нуж­но ес­ли на­до очис­тить то, что бы­ло на­ри­со­ва­но до это­го.
    В чем раз­ни­ца - ес­ли вам на­до ри­со­вать толь­ко в прог­рам­ме, ис­поль­зуй­те pa­nel, оно про­ще и мень­ше ре­сур­сов ест. Ес­ли ва­ша прог­рам­ма ра­бо­та­ет с изоб­ра­же­ни­ями, в том чис­ле с фай­ла­ми изоб­ра­же­ний - заг­руз­ка/сох­ра­не­ние, ри­суй­те в pic­tu­re­Box.Ima­ge, так про­ще сох­ра­нять, во-пер­вых, и pic­tu­re­Box соз­дан для хра­не­ния изоб­ра­же­ний­, так что де­шев­ле за­пи­сать Ima­ge в pic­tu­re­Box.Ima­ge, чем вы­зы­вать в pa­nel1_Pa­int e.Grap­hics.Dra­wI­ma­ge().
    Но не за­бы­вай­те при заг­руз­ке фор­мы соз­да­вать Ima­ge в pic­tu­re­Box, ибо по умол­ча­нию, pic­tu­re­Box.Ima­ge = null;
    Для ра­дио кно­пок:
    
    private vo­id ra­di­oBut­ton1_Chec­ked­C­han­ged(obj­ect sen­der, Even­tArgs e) {
    panel1.Refresh();
    // или
    pictureBox1.Refresh();
    }
    
    Основные ошиб­ки
    Идеология GDI+ по­че­му-то у мно­гих вы­зы­ва­ет ку­чу проб­лем... ско­рее все­го, по­то­му что лю­ди не по­ни­ма­ют прин­цип ра­бо­ты Win­dows.
    Ошибка 1. Про­па­да­ющее изоб­ра­же­ние - лю­ди ри­су­ют в pa­nel, или pic­tu­re­Box не в event Pa­int, а по на­жа­тию кноп­ки, или от­ку­да-то еще и удив­ля­ют­ся, по­че­му их изоб­ра­же­ние не пе­ре­ри­со­вы­ва­ет­ся, а про­па­да­ет, ког­да фор­ма пе­ре­ри­со­вы­ва­ет­ся (ее свер­ну­ли/раз­вер­ну­ли, уб­ра­ли за эк­ран/вы­ве­ли об­рат­но и пр.).
    Ответ 1. Так и дол­ж­но быть - ког­да вы ри­су­ете по кон­т­ро­лу - вы ри­су­ете на эк­ра­не, ког­да эк­ран пе­ре­ри­со­вы­ва­ет­ся, он пе­ре­ри­со­вы­ва­ет­ся с ну­ля, и ес­ли ваш код ри­со­ва­ния не вклю­чен в це­поч­ку пе­ре­ри­со­вы­ва­ния - ва­ше­го ри­сун­ка и не бу­дет. Что­бы вклю­чить ваш код в це­поч­ку - пос­тавь­те его в event Pa­int нуж­но­го кон­т­ро­ла.
    Ошибка 2. При ри­со­ва­нии в кон­т­ро­ле pic­tu­re­Box, лю­ди ри­су­ют по кон­т­ро­лу, а не по Ima­ge.
    Ответ 2. Ког­да ри­су­ете - по­лу­чай­те Grap­hics че­рез Grap­hics.Fro­mI­ma­ge(pic­tu­re­Box.Ima­ge), a не e.Grap­hics.
    Ошибка 3. При ис­поль­зо­ва­нии pic­tu­re­Box - соз­да­ет­ся но­вый Ima­ge, в нем про­во­дит­ся ри­со­ва­ние, по­том он встав­ля­ет­ся в pic­tu­re­Box.
    Ответ 3. На хре­на соз­да­вать но­вый Ima­ge, каж­дый раз ког­да вы ри­су­ете. Соз­да­вать но­вый на­до толь­ко ког­да вы ме­ня­ете раз­мер pic­tu­re­Box.
    
    И пос­лед­нее, нес­мот­ря не не­дос­тат­ки тех­но­ло­гии сбо­ра му­со­ра (Gar­ba­ge Col­lec­tor), ре­ко­мен­ду­ет­ся им поль­зо­вать­ся. Во-пер­вых, ес­ли это вой­дет в при­выч­ку сей­час, то ког­да тех­но­ло­гия за­ра­бо­та­ет (нап­ри­мер, в .NET 4.0), вы бу­де­те пи­сать пра­виль­но. Во-вто­рых, тех­но­ло­гия и сей­час ра­бо­та­ет с боль­ши­ми объ­ема­ми в па­мя­ти, а изоб­ра­же­ния (и мно­гие дру­гие объ­ек­ты ри­со­ва­ния) от­но­сят­ся имен­но к боль­шим объ­емам. Так что лю­бой соз­дан­ный ва­ми объ­ект Grap­hics дол­жен быть Dis­po­se() пос­ле окон­ча­ния ра­бо­ты с ним, лю­бой соз­дан­ный Ima­ge дол­жен быть Dis­po­se() пос­ле окон­ча­ния ра­бо­ты с ним. И глав­ное, пос­ле всех Dis­po­se() - не за­бы­вай­те вы­зы­вать GC.Col­lect().
    
    Знающим ан­г­лий­ский мо­гу по­ре­ко­мен­до­вать сайт Bob Po­well (http://www.bobpowell.net/), как ис­точ­ник ку­чи по­лез­ной ин­фор­ма­ции для на­чи­на­ющих в GDI+.
    
Глава 2. Объект Graphics.
    
    Как уже го­во­ри­лось, Grap­hics - ос­нов­ной объ­ект (и класс) для ри­со­ва­ния. Рас­смот­рим под­роб­но, что он уме­ет.
    Конструкторы
    У клас­са есть ста­ти­чес­кие фун­к­ции для соз­да­ния объ­ек­та Grap­hics, и об­ра­ти­те вни­ма­ние - нет pub­lic кон­с­т­рук­то­ра. По­это­му соз­да­вать объ­ект при­хо­дить­ся од­ним из сле­ду­ющих спо­со­бов:
    FromHdc - по ука­за­те­лю на кон­текст ус­т­рой­ст­ва (De­vi­ce Con­text) - ис­поль­зу­ет­ся ред­ко.
    FromHwnd - по ука­за­те­лю на кон­т­рол (или фор­му).
    
    Graphics gr = Grap­hics.From­H­w­nd(pa­nel1.Han­d­le);
    
    FromImage - для ри­сун­ка (Ima­ge).
    
    Graphics gr = Grap­hics.Fro­mI­ma­ge(bit­map);
    
    Поле ри­со­ва­ния
    Объект Grap­hics при­вя­зан к оп­ре­де­лен­но­му по­лю ри­со­ва­ния (Clip), ко­то­рое име­ет при­вяз­ку к объ­ек­ту, ко­ор­ди­на­ты и раз­ме­ры. Очень удоб­ная вещь с точ­ки зре­ния двух мо­мен­тов - во-пер­вых, вы мо­же­те из­ме­нять об­ласть, в ко­то­рой про­ис­хо­дит соб­с­т­вен­но ри­со­ва­ние, не из­ме­няя ко­да ри­со­ва­ния. Нап­ри­мер, вам на­до ку­сок су­щес­т­ву­ющей кар­тин­ки пе­рек­ра­сить - вы мо­же­те зак­ра­сить этот ку­сок, ука­зав точ­но все его ко­ор­ди­на­ты, а мо­же­те за­лить всю кар­тин­ку че­рез Cle­ar, из­ме­нив раз­ме­ры и ко­ор­ди­на­ты Clip так, что­бы они сов­па­ли с нуж­ным вам кус­ком. И во-вто­рых, ри­со­ва­ние ве­дет­ся толь­ко в пре­де­лах Clip ре­ги­она, т.е. на все, что ри­су­ет­ся вне его вре­мя и ре­сур­сы не тра­тят­ся.
    
    В клас­се есть на­бор фун­к­ций для уп­рав­ле­ния ко­ор­ди­на­та­ми и раз­ме­ра­ми Clip:
    свойство Clip - воз­в­ра­ща­ет Re­gi­on в ко­то­ром ве­дет­ся ри­со­ва­ние, так­же поз­во­ля­ет ус­та­но­вить но­вый Re­gi­on нап­ря­мую.
    свойство Clip­Bo­unds - воз­в­ра­ща­ет пря­мо­уголь­ник, опи­сы­ва­ющий ре­ги­он Clip.
    свойство Is­C­li­pEmpty - по­ка­зы­ва­ет пуст ли Clip.
    свойство Is­Vi­sib­leC­li­pEmpty - то же, но с про­вер­кой ви­ди­мой час­ти Clip. Ра­бо­та­ет толь­ко при ри­со­ва­нии по кон­т­ро­лу.
    IsVisible - про­ве­ря­ет, ви­дим ли дан­ный пря­мо­уголь­ник. Очень удоб­но, ес­ли вы ри­су­ете в кон­т­ро­ле что-то, что тре­бу­ет боль­ших и дол­гих рас­че­тов, преж­де чем счи­тать - про­верь­те, а по­ка­за­но-то оно бу­дет.
    ExcludeClip - ис­к­лю­ча­ет из Clip фи­гу­ру, пе­ре­дан­ную как ар­гу­мент.
    IntersectClip - ос­тав­ля­ет в Clip толь­ко об­ласть пе­ре­се­че­ния Clip и фи­гу­ры в ар­гу­мен­те.
    ResetClip - ус­та­нав­ли­ва­ет Clip рав­ным бес­ко­неч­нос­ти.
    SetClip - ус­та­нав­ли­ва­ет Clip из ар­гу­мен­тов.
    TranslateClip - cме­ща­ет Clip по плос­кос­ти ри­со­ва­ния.
    
    Разница меж­ду Clip и ви­ди­мой час­тью Clip: ес­ли у вас есть pa­nel кон­т­рол, с вклю­чен­ным AutoS­c­roll и со­дер­жи­мое пре­вы­ша­ет раз­ме­ры pa­nel, то соз­да­вая Grap­hics по ука­за­те­лю на кон­т­рол, вы по­лу­чи­те Clip = всей об­лас­ти pa­nel, од­на­ко ви­ди­мая часть Clip - это та, ко­то­рая по­ка­зы­ва­ет­ся в нас­то­ящий мо­мент поль­зо­ва­те­лю. Но пом­ни­те, что ви­ди­мая часть Clip име­ет ко­ор­ди­на­ты и раз­мер кон­т­ро­ла - т.е. в слу­чае pa­nel, со скрол­лом, сме­щен­ным впра­во до кон­ца, ви­ди­мый клип все рав­но бу­дет от 0;0 до pa­nel.Wid­th;pa­nel.He­ight.
    
    Рисование и за­лив­ка
    Среди фун­к­ций объ­ек­та мож­но вы­де­лить две боль­шие груп­пы - для ри­со­ва­ния и для за­лив­ки.
    Почти все фун­к­ции, на­чи­на­ющи­еся со сло­ва Draw - ри­су­ют за­дан­ную фи­гу­ру за­дан­ной руч­кой.
    Все фун­к­ции, на­чи­на­ющи­еся со сло­ва Fill - за­ли­ва­ют за­дан­ную фи­гу­ру за­дан­ной кис­тью.
    Кисти и руч­ки бу­дут рас­смот­ре­ны в сле­ду­ющем пос­те.
    Список фун­к­ций весь­ма ве­лик:
    DrawArc - ри­су­ет ду­гу
    DrawLine - ри­су­ет ли­нию
    DrawPolygon - ри­су­ет мно­го­уголь­ник и т.д.
    Аналогично, фун­к­ции Fil­lArc, Fil­lPol­y­gon и т.д.
    
    Дополнительное ри­со­ва­ние
    Дополнительно к ри­со­ва­нию прос­тых форм есть сле­ду­ющие фун­к­ции:
    DrawString - ри­су­ет стро­ку, и за­ли­ва­ет ее за­дан­ной кис­тью.
    DrawIcon и Dra­wI­co­nUn­s­t­ret­c­hed - ри­су­ет икон­ку (объект Icon), и ри­су­ет икон­ку без из­ме­не­ния, со­от­вет­с­т­вен­но.
    DrawImage, Dra­wI­ma­ge­Un­s­ca­led и Dra­wI­ma­ge­Un­s­ca­le­dAn­d­C­lip­ped - ри­су­ет кар­тин­ку (объект Ima­ge), ри­су­ет кар­тин­ку без из­ме­не­ний в ука­зан­ной точ­ке и ри­су­ет кар­тин­ку без из­ме­не­ний с об­рез­кой по ука­зан­но­му пря­мо­уголь­ни­ку со­от­вет­с­т­вен­но.
    Clear - за­ли­ва­ет все по­ле ри­со­ва­ния ука­зан­ным цве­том.
    CopyFromScreen - ко­пи­ру­ет, по­пик­сель­но, изоб­ра­же­ние на эк­ра­не в ука­зан­ном пря­мо­уголь­ни­ке в ука­зан­ный пря­мо­уголь­ник по­ля ри­со­ва­ния.
    
    Текст
    Для ри­со­ва­ния тек­с­та есть вспо­мо­га­тель­ные фун­к­ции:
    MeasureString - поз­во­ля­ет по­лу­чить раз­ме­ры стро­ки, ког­да она бу­дет на­ри­со­ва­на.
    MeasureCharacterRanges - поз­во­ля­ет по­лу­чить раз­ме­ры на­бо­ра сим­во­лов, ког­да они бу­дут на­ри­со­ва­ны.
    Разница меж­ду фун­к­ци­ями в раз­ном под­хо­де к оп­ре­де­ле­нию до­пус­ков на сви­са­ющие час­ти букв, раз­ный до­пуск на сгла­жи­ва­ние и еще чуть-чуть. Под­с­чет в лю­бом слу­чае не иде­аль­ный­, так как фун­к­ции по­че­му-то не ис­поль­зу­ют ус­та­нов­лен­ный па­ра­метр ти­па сгла­жи­ва­ния шриф­та в сис­те­ме и в объ­ек­те Grap­hics.
    свойство Tex­t­Ren­de­rin­g­Hint - оп­ре­де­ля­ет ре­жим сгла­жи­ва­ния тек­с­та. Ва­ри­ан­ты - без сгла­жи­ва­ния (Sin­g­le­Bit­Per­Pi­xel) и со сгла­жи­ва­ни­ем (Anti­Ali­as), каж­дый мо­жет быть с под­с­т­рой­кой сви­са­ющих час­тей (Grid­Fit) или без. Плюс есть Cle­ar­T­y­peG­rid­Fit - ри­су­ет че­рез дви­жок Cle­ar­T­y­pe. И есть ва­ри­ант System­De­fa­ult - ис­поль­зо­вать нас­т­рой­ки сис­те­мы.
    свойство Tex­t­Con­t­rast - оп­ре­де­ля­ет кон­т­рас­т­ность тек­с­та, ес­ли в сис­те­ме вклю­че­но сгла­жи­ва­ние тек­с­та или Cle­ar­T­y­pe.
    
    Преобразования
    При ри­со­ва­нии до­воль­но час­то не­об­хо­ди­мо про­вес­ти над изоб­ра­же­ни­ем, или бу­ду­щим изоб­ра­же­ни­ем, не­кие тран­с­фор­ма­ции - из­ме­нить мас­ш­таб, по­вер­нуть, мо­жет быть под­ви­нуть, не ме­няя ос­нов­но­го ко­да ри­со­ва­ния. Для это­го в GDI+ есть класс Mat­rix, опи­сы­ва­ющий век­тор­ные (ко­ор­ди­нат­ные) тран­с­фор­ма­ции для каж­дой ри­су­емой точ­ки. Для тех кто не зна­ет, или уже бла­го­по­луч­но за­был что та­кое мат­ри­цы и как с ни­ми ра­бо­та­ют, есть фун­к­ции, ко­то­рые вы­пол­ня­ют опе­ра­ции над мат­ри­цей за вас.
    свойство Tran­s­form - воз­в­ра­ща­ет и поз­во­ля­ет за­дать мат­ри­цу пре­об­ра­зо­ва­ний.
    MultiplyTransform - пе­рем­но­жа­ет те­ку­щую мат­ри­цу и мат­ри­цу в ар­гу­мен­те в за­дан­ном по­ряд­ке.
    TranslateTransform - пе­ред­ви­га­ет ри­со­ва­ние по плос­кос­ти.
    RotateTransform - по­во­ра­чи­ва­ет ри­со­ва­ние от­но­си­тель­но на­ча­ла ко­ор­ди­нат.
    ResetTransform - об­ну­ля­ет все тран­с­фор­ма­ции.
    ScaleTransform - из­ме­ня­ет мас­ш­таб, по каж­дой оси от­дель­но.
    TransformPoints - пре­об­ра­зу­ет ко­ор­ди­на­ты за­дан­но­го мас­си­ва то­чек из од­ной сис­те­мы ко­ор­ди­нат в дру­гую.
    
    Качество
    Еще есть на­бор свойств, оп­ре­де­ля­ющих ка­чес­т­во ри­со­ва­ния.
    CompositingMode - оп­ре­де­ля­ет как ри­сун­ки (Ima­ge) бу­дут ри­со­вать­ся. Ва­ри­ан­ты So­ur­ce­Copy - цвет ри­сун­ка пе­рек­ры­ва­ет под­лож­ку, So­ur­ce­Over - цвет ри­сун­ка сме­ши­ва­ет­ся с цве­том под­лож­ки, в про­пор­ции, оп­ре­де­ля­емой аль­фа ком­по­нен­той цве­та.
    CompositingQuality - оп­ре­де­ля­ет ка­чес­т­во пре­об­ра­зо­ва­ния ри­сун­ков (Ima­ge), ког­да они ри­су­ет­ся один по дру­го­му. Из ва­ри­ан­тов ре­аль­но поль­зо­вать­ся сто­ит толь­ко Hig­h­Qu­ality - ка­чес­т­вен­но, но мед­лен­но и Hig­h­S­pe­ed - быс­т­ро, но не очень ка­чес­т­вен­но.
    DpiX и DpiY - поз­во­ля­ют уз­нать dpi по обе­им осям.
    InterpolationMode - оп­ре­де­ля­ет ме­тод ин­тер­по­ля­ции, по су­ти - сгла­жи­ва­ние, при ри­со­ва­нии. Ва­ри­ан­ты для ис­поль­зо­ва­ния в по­ряд­ке воз­рас­та­ния ка­чес­т­ва и вре­ме­ни: Ne­ares­t­Ne­ig­h­bor, Bi­li­ne­ar, Hig­h­Qu­alit­y­Bi­li­ne­ar, Bi­cu­bic, Hig­h­Qu­alit­y­Bi­cu­bic.
    PixelOffsetMode - оп­ре­де­ля­ет ка­чес­т­во of­fset пик­се­лей­, чтоб я по­ни­мал что это та­кое :). Нас­коль­ко я по­нял, это то­же па­ра­метр сгла­жи­ва­ния, но на уров­не сме­ше­ния цве­тов пик­се­лей. Для ис­поль­зо­ва­ния обыч­ные два ва­ри­ан­та - Hig­h­Qu­ality и Hig­h­S­pe­ed, и ва­ри­ант No­ne - ни­ка­кой об­ра­бот­ки.
    SmoothingMode - оп­ре­де­ля­ет ре­жим сгла­жи­ва­ния ли­ний. Те же ва­ри­ан­ты - Hig­h­Qu­ality, Hig­h­S­pe­ed и No­ne.
    
    Прочее
    Есть еще на­бор фун­к­ций­, ко­то­рые ни к ка­кой груп­пе не от­но­сять­ся, но иног­да они нуж­ны:
    GetNearestColor - воз­в­ра­ща­ет бли­жай­щий цвет к ар­гу­мен­ту в цве­то­вом прос­т­ран­с­т­ве объ­ек­та Grap­hics.
    Save - поз­во­ля­ет сох­ра­нить сос­то­яние объ­ек­та Grap­hics: тран­с­фор­ма­ции, Clip, ка­чес­т­во.
    Restore - поз­во­ля­ет во­ос­та­но­вить сос­то­янии объ­ект из ра­нее сох­ра­нен­но­го.
    
    И пос­лед­нее - есть еще фун­к­ции для уп­рав­ле­ния ме­та­фай­ла­ми, кон­тей­не­ра­ми и еще нес­коль­ко вспо­мо­га­тель­ных. Их не бу­дет в даль­ней­ших при­ме­рах, я ими не поль­зо­вал­ся ни­ког­да, и ду­маю, что ес­ли они ко­му нуж­ны - эти лю­ди спо­соб­ны са­ми ра­зоб­рать­ся.
    
Глава 3. Цвета.
    
    Прежде чем на­чать ри­со­вать, на­до бы ра­зоб­рать­ся с цве­та­ми и их ис­поль­зо­ва­ни­ем. Мы бу­дем рас­смат­ри­вать толь­ко воп­ро­сы оп­ре­де­ле­ния и вы­бо­ра цве­тов в ком­пь­ютер­ных цве­то­вых прос­т­ран­с­т­вах, и не бу­дем ка­сать­ся би­оло­го-ху­до­жес­т­вен­ных ас­пек­тов. Хо­тя есть нес­коль­ко мо­мен­тов, ко­то­рые не­об­хо­ди­мо знать и пом­нить:
    1. вос­п­ри­ятие цве­тов у каж­до­го че­ло­ве­ка ин­ди­ви­ду­аль­но.
    2. Каж­дый мо­ни­тор/прин­тер/и т.д. по­ка­зы­ва­ют один и тот же цвет (с точ­ки зре­ния цифр) по-раз­но­му. Бо­лее то­го, боль­шин­с­т­во мо­ни­то­ров по­ка­зы­ва­ют один и тот же цвет по-раз­но­му в раз­ных час­тях эк­ра­на.
    
    Цветовые прос­т­ран­с­т­ва
    Я ду­маю все зна­ют из­вес­т­ный пос­ту­лат - лю­бой цвет мож­но по­лу­чить сме­ше­ни­ем трех, так на­зы­ва­емых, ос­нов­ных. Поп­рав­ка пер­вая: "лю­бой цвет" - это лю­бой из па­лит­ры, вос­п­ри­ни­ма­емой че­ло­ве­чес­ким гла­зом. Ус­то­яв­ше­еся мне­ние гла­сит, что че­ло­ве­чес­кий глаз раз­ли­ча­ет все­го око­ло 16 мил­ли­онов цве­тов. Су­щес­т­ву­ют мно­гие не­сог­лас­ные с этим, но боль­шин­с­т­во ра­бо­та­ет имен­но с 2^24 цве­та­ми, и мы бу­дем рас­смат­ри­вать имен­но та­кие прос­т­ран­с­т­ва. Од­на­ко, на­до знать, что су­щес­т­ву­ют цве­то­вые прос­т­ран­с­т­ва пос­т­ро­ен­ные на 4 цве­тах (нап­ри­мер, CMYK) и на 6 цве­тах. Впро­чем, 4-х цвет­ные наш­ли при­ме­не­ние толь­ко в по­лиг­ра­фии, а с 6-ти цвет­ны­ми я стал­ки­вал­ся толь­ко в те­ории, и не знаю где они при­ме­ня­ют­ся.
    Итак, трех-осе­вые цве­то­вые прос­т­ран­с­т­ва, са­мое из­вес­т­ное из них - RGB - Red (Крас­ный­) Gre­en (Зе­ле­ный­) Blue (Си­ний­). Ис­поль­зу­ет­ся в ЭЛТ мо­ни­то­рах, во мно­гих прин­те­рах и во мно­гих фор­ма­тах фай­лов. Сме­ши­вая эти три ос­нов­ные цве­та в раз­ных про­пор­ци­ях мож­но по­лу­чить "лю­бой­" цвет. От­ве­дя по 8 бит на цвет мы по­лу­ча­ем 2^24 = 16777216 цве­тов, что, как при­ня­то, опи­сы­ва­ет все цве­та, вос­п­ри­ни­ма­емые че­ло­ве­чес­ким гла­зом. Все до­воль­ны.
    Второе по по­пу­ляр­нос­ти прос­т­ран­с­т­во - HSB (HSL/HSV) - Hue (Цвет­ность) Sa­tu­ra­ti­on (На­сы­щен­ность) Brig­h­t­ness (Яркость) / Lig­h­t­ness (Осве­щен­ность) / Va­lue (Зна­че­ние). Ис­поль­зу­ет­ся прак­ти­чес­ки во всех гра­фи­чес­ких ре­дак­то­рах. Кста­ти, в стан­дар­т­ном ди­ало­ге вы­бо­ра цве­та Win­dows ис­поль­зу­ет­ся имен­но это прос­т­ран­с­т­во:
    
    В дан­ном слу­чае все раз­ри­со­ва­но в фор­ме квад­ра­та, что не­вер­но. Hue - это па­ра­метр, обоз­на­ча­ющий угол на цве­то­вом кру­ге. Справ­ка: цве­то­вой круг Ге­те, са­мый из­вес­т­ный­, при всей сво­ей пра­виль­нос­ти не был при­нят в тех­но­ло­ги­чес­ком ми­ре. Од­на­ко, имен­но он пос­лу­жил ос­но­вой для соз­да­ния прос­т­ран­с­т­ва HSB. Бо­лее пра­виль­ная фор­ма вы­бо­ра цве­та в прос­т­ран­с­т­ве HSB та­кая:
    
    C прос­т­ран­с­т­вом HSB свя­за­на нап­ри­ят­ная проб­ле­ма - па­ра­метр Hue дол­жен при­ни­мать зна­че­ния от 0 до 360, что ни­как не ук­ла­ды­ва­ет­ся в нор­маль­ную дво­ич­ную сис­те­му за­пи­си дан­ных... Да и для Sa­tu­ra­ti­on и Brig­h­t­ness еди­но­го мне­ния нет - кто-то счи­та­ет что зна­че­ния дол­ж­ны быть от 0 до 100, кто-то от 0 до 1, кто-то от 0 до 240... По­это­му, нес­мот­ря на то, что ра­бо­тать мно­гие пред­по­чи­та­ют в нем, сох­ра­не­ние дан­ных ве­дет­ся в RGB, бла­го оба прос­т­ран­с­т­ва вза­имо­кон­вер­ти­ру­емые, хо­тя тут есть нес­коль­ко ло­ву­шек, об этом ни­же.
    Каждая ось лю­бо­го цве­то­во­го прос­т­ран­с­т­ва так­же на­зы­ва­ет­ся ка­на­лом - крас­ный ка­нал, или ка­нал крас­но­го и т.п.
    Существует еще око­ло 10-15 цве­то­вых прос­т­ранств, но сре­ди Win­dows прог­рам­мис­тов они очень ма­ло рас­п­рос­т­ра­не­ны и мы их рас­смат­ри­вать не бу­дем.
    
    Битность цве­та
    Битность цве­та - па­ра­метр, оп­ре­де­ля­ющий сколь­ко бит па­мя­ти при­хо­дит­ся на цвет каж­до­го пик­се­ля. Ес­ли вдруг кто не зна­ет: пик­сель - ми­ни­маль­ная еди­ни­ца пло­ща­ди эк­ра­на/рас­т­ро­во­го ри­сун­ка, т.е. точ­ка.
    Я уже упо­ми­нал, что на каж­дый из трех ос­нов­ных цве­тов вы­де­ли­ли по 8 бит и всем ста­ло хо­ро­шо. Од­на­ко, это слу­чи­лось не так дав­но, а до это­го вы­де­лять по 3 бай­та на один пик­сель бы­ло не­поз­во­ли­тель­ной рос­кошью. Ис­то­рия раз­ви­тия при­мер­но та­кая:
    1. Мо­нох­ро­мы - один цвет. Мо­жет кто пом­нит, бы­ли та­кие мо­ни­то­ры, ко­то­рые по­ка­зы­ва­ли все ис­к­лю­чи­тель­но ядо­ви­то-зе­ле­ным цве­том.
    2. 4-х цвет­ные. Бы­ла та­кая вещь, од­на­ко дол­го не про­жи­ла, пос­коль­ку ее быс­т­ро сме­ни­ли.
    3. 16-ти цвет­ные. Это уже на­ча­ло нор­маль­но­го цве­та в ком­пь­юте­ре. На каж­дый пик­сель вы­де­ля­лось 4 би­та, они опи­сы­ва­ли один из 16 из­вес­т­ных ком­пу цве­тов. Но, как вы­яс­ни­лось поз­же, это то­же бы­ло вре­мен­но - мо­ни­то­ры ста­ли по­ка­зы­вать все 16 мил­ли­онов, а ком­пы не мог­ли столь­ко вы­дать од­нов­ре­мен­но... И тог­да при­ду­ма­ли вы­ход.
    4. 256 цве­тов. На каж­дый пик­сель вы­де­лял­ся байт па­мя­ти, ко­то­рый мог опи­сать цвет. Но тог­да же по­яви­лась идея па­литр - быйт па­мя­ти оп­ре­де­лял но­мер цве­та в па­лит­ре, а са­ма па­лит­ра в 256 цве­тов вы­би­ра­лась из пол­но­го на­бо­ра.
    5. Даль­ше все прос­то - с рос­том ком­пь­ютер­ной мощ­нос­ти и объ­емов па­мя­ти по­явил­ся 16 бит­ный цвет - 65535 цве­тов, ни­ка­ких па­литр, и выг­ля­дит все впол­не прис­той­но. По­том 24 би­та - 16 мил­ли­онов цве­тов, с ко­то­ры­ми сей­час все и ра­бо­та­ют.
    6. 32 и 48 бит: 48 бит я по­ка в де­ле не ви­дел, од­на­ко он есть, под не­го есть кар­ты и т.д. 32 би­та - име­ет два при­ме­не­ния, во-пер­вых для 4-х цвет­ных прос­т­ранств, а во-вто­рых для под­дер­ж­ки проз­рач­нос­ти, об этом чуть ни­же.
    
    Прозрачность
    Сначала проз­рач­ность по­яви­лась в би­то­вой фор­ме - т.е. пик­сель или пол­нос­тью проз­рач­ный или цвет­ной. Проз­рач­ность в gif сде­ла­на имен­но так. По­том уже по­яви­лась гра­да­ци­он­ная проз­рач­ность. Для нее сде­ла­ли 256 гра­да­ций­, т.е. вы­де­ли­ли еще байт на хра­не­ние проз­рач­нос­ти пик­се­ля. Байт, хра­ня­щий проз­рач­ность по­лу­чил наз­ва­ние аль­фа. До­бав­ле­ние аль­фа ка­на­ла не соз­да­ет до­пол­ни­тель­ной оси прос­т­ран­с­т­ва. Это не ком­по­нент цве­та в пря­мом смыс­ле, - это па­ра­метр, по­ка­зы­ва­ющий­, в ка­кой про­пор­ции на­до сме­ши­вать этот цвет, с ле­жа­щим "ни­же". Сре­ди рас­п­рос­т­ра­нен­ных фор­ма­тов фай­лов толь­ко png под­дер­жи­ва­ет аль­фа-ка­нал.
    К сло­ву ска­зать, Win­dows до сих пор уме­ет нор­маль­но ра­бо­тать толь­ко с би­то­вой проз­рач­нос­тью, го­во­рят Vis­ta на­учи­лась ра­бо­тать с гра­да­ци­он­ной­, но это мы пос­мот­рим пос­ле ре­ли­за.
    
    Форматы цве­тов
    Итак, сов­ме­щая вы­ше­опи­сан­ное в раз­ных ком­би­на­ци­ях мы по­лу­ча­ем фор­ма­ты цве­тов/цвет­нос­ти. Нап­ри­мер, один из са­мых сей­час рас­п­рос­т­ра­нен­ных фор­ма­тов - 32bppARGB: 32 би­та на пик­сель, цве­то­вое прос­т­ран­с­т­во RGB с под­дер­ж­кой аль­фа-ка­на­ла. Фор­ма­тов су­щес­т­ву­ет мно­жес­т­во, но все они по­нят­ны из наз­ва­ния, я пе­ре­чис­лю и опи­шу толь­ко те, ко­то­рые ис­поль­зу­ют­ся в .NET 2.0 и со­дер­жать­ся в пе­ре­чис­ле­нии System.Dra­wing.Ima­ging.Pi­xel­For­mat:
    Alpha - каж­дый пик­сель со­дер­жит толь­ко аль­фа-ка­нал. 8бит.
    Canonical - 32 би­та, ARGB. Зна­че­ния в RGB ка­на­лах не из­ме­не­ны.
    DontCare - не ука­зы­вать фор­мат.
    Extended - не ис­поль­зу­ет­ся.
    Format16bppArgb1555 - 16 бит на пик­сель, 1 бит на проз­рач­ность и по 5 бит на цве­то­вые ка­на­лы. Та­ким об­ра­зом по­лу­ча­ем 32768 цве­тов и би­то­вую проз­рач­ность.
    Format16bppRgb555 - 16 бит. по 5 бит на цве­то­вой ка­нал, 1 бит не ис­поль­зу­ет­ся. Те же 32768 цве­тов, но без проз­рач­нос­ти.
    Format16bppRgb565 - 16 бит, по 5 бит на крас­ный и си­ний и 6 бит на зе­ле­ный. По­лу­ча­ем 65536 цве­тов.
    Format24bppRgb - 24 би­та, по 8 на каж­дый цвет. Са­мый рас­п­рос­т­ра­нен­ный сей­час фор­мат без проз­рач­нос­ти.
    Format32bppArgb - 32 би­та, по 8 на каж­дый цвет и 8 на аль­фа-ка­нал. Са­мый рас­п­рос­т­ра­нен­ный сей­час фор­мат с проз­рач­нос­тью.
    Format32bppPArgb - То же, что и пре­ды­ду­щий­, но зна­че­ния цве­то­вых ка­на­лов пре­об­ра­зо­ва­ны в со­от­вет­с­т­вии с аль­фа зна­че­ни­ем.
    Format32bppRgb - 32 би­та, по 8 на ка­нал и 8 не ис­поль­зу­ют­ся.
    Format48bppRgb - 48 бит, по 16 на каж­дый ка­нал.
    Format64bppArgb - 64 би­та, по 16 на цве­то­вой ка­нал, и 16 на проз­рач­ность.
    Format64bppPArgb - то же, что пре­ды­ду­щий­, но зна­че­ния в цве­то­вых ка­нал пре­об­ра­зо­ва­ны в со­от­вет­с­т­вии с аль­фа зна­че­ни­ем.
    
    Format1bppIndexed - ин­дек­си­ро­ван­ные цве­та, т.е. за­вя­зан­ные на па­лит­ру. Па­лит­ра из двух цве­тов. 1 бит на пик­сель.
    Format4bppIndexed - 4 би­та, па­лит­ра из 16 цве­тов.
    Format8bppIndexed - 8 бит, па­лит­ра из 256 цве­тов.
    
    Format16bppGrayScale - 65536 гра­да­ций се­ро­го.
    
    Лично я поль­зо­вал­ся толь­ко 24bppRGB, 32bppARGB, 8bppIn­de­xed и 16bppGray­S­ca­le. При соз­да­нии ма­сок в гра­фи­чес­ких ре­дак­то­рах час­то ис­поль­зу­ют од­но­би­то­вые фор­ма­ты, т.е. Al­p­ha впол­не мо­жет по­на­до­бить­ся. Ос­таль­ные, на мой взгляд, ос­тав­ле­ны для сов­мес­ти­мос­ти со ста­ры­ми фор­ма­та­ми и с бу­ду­щи­ми фор­ма­та­ми.
    
    Использование цве­тов в .NET
    Для ра­бо­ты с цве­та­ми есть класс Co­lor. Он со­дер­жит на­бор ста­ти­чес­ких свойств, ко­то­рые оп­ре­де­ля­ют рас­п­рос­т­ра­нен­ные цве­та. Нап­ри­мер, ес­ли вам ну­жен крас­ный цвет, про­ще все­го по­лу­чить его так:
    
    Color крас­ный = Co­lor.Red;
    
    Для нес­тан­дар­т­ных цве­тов есть фун­к­ция Fro­mArgb():
    
    Color стран­ный­Цвет = Co­lor.Fro­mArgb(255, 120, 12, 211);
    
    Есть еще две ста­ти­чес­кие фун­к­ции для соз­да­ния цве­та:
    FromKnownColor - соз­да­ет цвет из спис­ка из­вес­т­ных цве­тов, т.е. из пе­ре­чис­ле­ния Know­n­Co­lor, в ко­то­рое вхо­дят все стан­дар­т­ные цве­та и все сис­тем­ные цве­та.
    FromName - соз­да­ет цвет из стро­ки с име­нем цве­та.
    Помимо пе­ре­чис­ле­ния Know­Co­lor есть еще пе­ре­чис­ле­ние System­Co­lors, ко­то­рое со­дер­жит толь­ко сис­тем­ные цве­та - цве­та ра­мок, кно­пок, об­лас­ти ок­на и пр.
    
    У клас­са Co­lor есть еще не ста­ти­чес­кие чле­ны:
    свойства A, R, G, B - воз­в­ра­ща­ют со­от­вет­с­т­ву­ющую ком­по­нен­ту цве­та.
    свойства Is­K­now­n­Co­lor, Is­Na­med­Co­lor, Is­S­y­s­tem­Co­lor - про­ве­ря­ют, яв­ля­ет­ся ли цвет "извес­т­ным", наз­ван­ным и сис­тем­ным со­от­вет­с­т­вен­но.
    свойство Na­me - воз­в­ра­ща­ет имя цве­та
    ToKnownColor - воз­в­ра­ща­ет член пе­ре­чис­ле­ния Know­n­Co­lor.
    GetHue, Get­Sa­tu­ra­ti­on, Get­B­rig­h­t­ness - воз­в­ра­ща­ют зна­че­ния цве­та для осей Hue, Sa­tu­ra­ti­on и Brig­h­t­ness прос­т­ран­с­т­ва HSB.
    свойство IsEmpty - про­ве­ря­ет был ли цвет ини­ци­али­зи­ро­ван.
    ToArgb - воз­в­ра­ща­ет Int32.
    
    Использование прос­т­ран­с­т­ва HSB
    Частая си­ту­ация для гра­фи­чес­ких прог­рамм - на­до под­с­вет­лить изоб­ра­же­ние, или по­ни­зить кон­т­рас­т­ность, или ин­вер­ти­ро­вать цвет­ность. Та­кие воп­ро­сы лег­ко ре­ша­ют­ся че­рез HSB прос­т­ран­с­т­во, од­на­ко очень слож­но ре­ша­ют­ся че­рез RGB. По­че­му в клас­се Co­lor есть воз­мож­ность по­лу­чить зна­че­ния HSB, но нет воз­мож­ность за­дать их - вещь не­объ­яс­ни­мая ни чем, кро­ме ску­до­умия про­ек­ти­ров­щи­ков MS, и от­сут­с­т­вия у них опы­та ра­бо­ты с гра­фи­кой.
    Сделано мно­жес­т­во клас­сов, поз­во­ля­ющих ра­бо­тать с цве­та­ми в .NET нор­маль­но, т.е. ис­поль­зуя оба прос­т­ран­с­т­ва - RGB и HSB. Вот ссыл­ки на два из них:
    Bob Po­well - http://www.bobpowell.net/RGBHSB.htm
    
    Для ин­те­ре­су­ющих­ся воп­ро­сом серь­ез­нее, и зна­ющих ан­г­лий­ский­, вот еще две хо­ро­шие ссыл­ки:
    FAQ по кон­вер­та­ции цве­тов - http://www.martinreddy.net/gfx/faqs/colorconv.faq
    
Гла­ва 4. Ка­ран­да­ши.
    
    Наконец-то прис­ту­па­ем к ри­со­ва­нию. Ду­маю, ни для ко­го не но­вость, что ос­нов­ны­ми ин­с­т­ру­мен­та­ми ри­со­ва­ния яв­ля­ют­ся ка­ран­да­ши и кис­ти. В .net де­ло об­с­то­ит так же - класс Pen опи­сы­ва­ет ка­ран­да­ши, клас­сы, по­рож­ден­ные от Brush - кис­ти.
    
    Карандаши
    Для опи­са­ния стан­дар­т­ных ка­ран­да­шей есть пе­ре­чис­ле­ние Pens. Оно со­дер­жит прос­тые ка­ран­да­ши, тол­щи­ной 1, для всех стан­дар­т­ных цве­тов.
    
    private vo­id pa­nel1_Pa­int(obj­ect sen­der, Pa­in­tE­ven­tArgs e) {
    e.Graphics.DrawRectangle(Pens.Blue, new Rec­tan­g­le(10, 50, 100, 100));
    }
    
    Весь даль­ней­ший код ри­со­ва­ния бу­ду пи­сать без име­ни фун­к­ции - он всег­да в event pa­nel1_Pa­int.
    
    Разумеется ка­ран­да­ши поз­во­ля­ют го­раз­до боль­ше, не­же­ли ри­со­вать прос­тые ли­нии. Рас­смот­рим свой­ст­ва клас­са Pen:
    Color - цвет, ес­ли ка­ран­даш од­ноц­вет­ный.
    Brush - кисть, оп­ре­де­ля­ющия спо­соб за­лив­ки ли­ний­, на­ри­со­ван­нх ка­ран­да­шом, ес­ли он не од­ноц­вет­ный.
    Width - тол­щи­на ка­ран­да­ша.
    PenType - тип ка­ран­да­ша, на де­ле - тип за­лив­ки. Свой­ст­во оп­ре­де­ля­ет­ся ти­пом Brush, пе­ре­дан­ной ка­ран­да­шу.
    
    Pen p1 = new Pen(Co­lor.Red, 5);
    Pen p2 = new Pen(new Hat­c­h­B­rush(Hat­c­h­S­t­y­le.So­lid­Di­amond, Co­lor.Yel­low, Co­lor.Hot­Pink), 10);
    Pen p3 = new Pen(new Li­ne­ar­G­ra­di­en­t­B­rush(new Po­int(10, 70), new Po­int(150, 70), Co­lor.Indi­go, Co­lor.Indi­an­Red), 15);
    e.Graphics.DrawLine(p1, 10, 10, 150, 10);
    e.Graphics.DrawLine(p2, 10, 40, 150, 40);
    e.Graphics.DrawLine(p3, 10, 70, 150, 70);
    
    
    DashStyle - стиль ли­нии. ва­ри­ан­ты: So­lid - сплош­ная, Dash - пун­к­тир­ная, Das­h­Dot - ти­ре-точ­ка, Das­h­Dot­Dot - ти­ре-точ­ка-точ­ка, Dot - точ­ки, и Cus­tom - за­да­ет­ся поль­зо­ва­те­лем. Ес­ли выб­ран Cus­tom, то для оп­ре­де­ле­ния сти­ля ис­поль­зу­ют­ся зна­че­ния в сле­ду­ющем свой­ст­ве.
    DashPattern - ри­су­нок ли­нии - мас­сив дроб­ных чи­сел опи­сы­ва­ющий дли­ну штриш­ков и про­пус­ков.
    DashOffset - рас­сто­янии от на­ча­ла ли­нии, с ко­то­ро­го ли­ния ста­но­вить­ся не сплош­ной­, а штриш­ка­ми.
    DashCap - око­неч­нос­ти штриш­ков. Ва­ри­на­ты: Flat - обыч­ный (квад­рат­ный­), Ro­und - скруг­лен­ный­, Tri­an­g­le - тре­уголь­ни­ком.
    
    Pen p1 = new Pen(Co­lor.Red, 15);
    p1.DashStyle = Das­h­S­t­y­le.Das­h­Dot;
    Pen p2 = new Pen(Co­lor.Black, 15);
    p2.DashStyle = Das­h­S­t­y­le.Das­h­Dot­Dot;
    p2.DashOffset = 30;
    p2.DashCap = Das­h­Cap.Ro­und;
    Pen p3 = new Pen(Co­lor.Ma­gen­ta, 15);
    p3.DashStyle = Das­h­S­t­y­le.Cus­tom;
    p3.DashCap = Das­h­Cap.Tri­an­g­le;
    p3.DashPattern = new flo­at[4] {5,3,10,3};
    e.Graphics.DrawLine(p1, 10, 10, 300, 10);
    e.Graphics.DrawLine(p2, 10, 40, 300, 40);
    e.Graphics.DrawLine(p3, 10, 70, 300, 70);
    
    
    CompoundArray - мас­сив дроб­ных зна­че­ний для оп­ре­де­ле­ния нес­коль­ких па­рал­лель­ных ли­ний­, зна­че­ния оп­ре­де­ля­ют от­мет­ки гра­ниц ли­ний и про­пус­ков в до­лях еди­ни­цы, пер­вое зна­че­ние - 0, пос­лед­нее - 1.
    
    Pen p1 = new Pen(Co­lor.Red, 15);
    p1.CompoundArray = new flo­at[] { 0.0f, 0.3f, 0.5f, 1.0f };
    e.Graphics.DrawLine(p1, 10, 10, 300, 10);
    
    
    StartCap - око­неч­ность ли­нии в на­ча­ле. Ва­ри­ан­ты: Flat - прос­той­, Ro­und - скруг­лен­ный­, Squ­are - квад­рат­ный­, Tri­an­g­le - тре­уголь­ный­, Ar­ro­wAn­c­hor - стрел­ка с яко­рем, Di­amon­dAn­c­hor - ромб с яко­рем, Ro­un­dAn­c­hor - скруг­лен­ный с яко­рем, Squ­are­An­c­hor - квад­рат­ный с яко­рем, Cus­tom - оп­ре­де­ля­ет­ся поль­зо­ва­те­лем. Ес­ли за­дан Cus­tom, то в ка­чес­т­ве око­неч­нос­ти ис­поль­зу­ет­ся Cus­tom­S­tar­t­Cap.
    EndCap - око­неч­ность ли­нии в кон­це. Ва­ри­ан­ты те же. В слу­чае Cus­tom - ис­поль­зу­ет­ся Cus­to­mEn­d­Cap.
    CustomStartCap - лич­ная око­неч­ность ли­нии, соз­да­ет­ся из Grap­hic­s­Path.
    CustomEndCap - лич­ная око­неч­ность ли­нии, соз­да­ет­ся из Grap­hic­s­Path.
    
    Pen p1 = new Pen(Co­lor.Red, 10);
    p1.StartCap = Li­ne­Cap.Arro­wAn­c­hor;
    p1.EndCap = Li­ne­Cap.Di­amon­dAn­c­hor;
    Pen p2 = new Pen(Co­lor.Black, 10);
    p2.StartCap = Li­ne­Cap.Ro­und;
    p2.EndCap = Li­ne­Cap.Ro­un­dAn­c­hor;
    Pen p3 = new Pen(Co­lor.Ma­gen­ta, 10);
    p3.StartCap = Li­ne­Cap.Squ­are;
    p3.EndCap = Li­ne­Cap.Squ­are­An­c­hor;
    Pen p4 = new Pen(Co­lor.Lig­h­t­G­re­en, 10);
    p4.StartCap = Li­ne­Cap.Tri­an­g­le;
    p4.EndCap = Li­ne­Cap.Flat;
    e.Graphics.DrawLine(p1, 10, 10, 300, 10);
    e.Graphics.DrawLine(p2, 10, 40, 300, 40);
    e.Graphics.DrawLine(p3, 10, 70, 300, 70);
    e.Graphics.DrawLine(p4, 10, 100, 300, 100);
    
    Как вид­но из ри­сун­ка, якорь - это уве­ли­чен­ная часть ли­нии, пред­наз­на­чен­ная для пре­дос­тав­ле­ния поль­зо­ва­те­лю воз­мож­нос­ти пе­ре­тас­ки­ва­ния кон­цов ли­нии.
    
    Alignment - вы­рав­ни­ва­ние ка­ран­да­ша. Весь­ма глюч­ная вещь. Из 5 ва­ри­ан­тов ра­бо­та­ют толь­ко 2 - Cen­te­red и In­set, при­чем In­set име­ет ряд ог­ра­ни­че­ний и глю­ков.
    
    Pen p1 = new Pen(Co­lor.Red, 10);
    Pen p2 = new Pen(Co­lor.Black, 10);
    p2.Alignment = Pe­nA­lig­n­ment.Inset;
    Pen p3 = new Pen(Co­lor.Whi­te, 1);
    e.Graphics.DrawRectangle(p1, new Rec­tan­g­le(10, 50, 100, 100));
    e.Graphics.DrawRectangle(p2, new Rec­tan­g­le(10, 50, 100, 100));
    e.Graphics.DrawRectangle(p3, new Rec­tan­g­le(10, 50, 100, 100));
    
    Как мож­но ви­деть - при стан­дар­т­ном зна­че­нии, тол­щи­на ка­ран­да­ша рас­п­ре­де­ля­ет­ся по­ров­ну, по обе сто­ро­ны от цен­т­раль­ной ли­нии, ко­то­рая от­ри­со­вы­ва­ет­ся ка­ран­да­шом с еди­нич­ной тол­щи­ной. В слу­чае In­set - вся тол­щи­на по­ме­ща­ет­ся внут­ри ли­нии, опи­сан­ной еди­нич­ной тол­щи­ны ка­ран­да­шом. На де­ле, при слож­ных фи­гу­рах, при зна­че­нии In­set ка­ран­даш иног­да вы­ле­за­ет за цен­т­раль­ную ли­нию.
    
    LineJoin - - оп­ре­де­ля­ет вид со­еди­не­ния ли­ний. Ва­ри­ан­ты: Be­vel - сре­за­ет угол, ос­тав­ля­ет ту­пой ко­нец; Ro­und - со­еди­ня­ет ду­гой­; Mi­ter - ос­т­рый ко­нец или ту­пой в за­ви­си­мос­ти от зна­че­ния Mi­ter­Li­mit; Mi­ter­C­lip­ped - ос­т­рый угол или сре­зан­ный­, в за­ви­си­мос­ти от зна­че­ния Mi­ter­Li­mit, меж­ду дву­мя пос­лед­ни­ми я раз­ни­цы не за­ме­тил.
    MiterLimit - оп­ре­де­ля­ет пре­дель­ную тол­щи­ну ли­нии в мес­те со­еди­не­ния двух от­рез­ков. Из­ме­ря­ет­ся в до­лях еди­ни­цы от тол­щи­ны ли­нии.
    
    Pen p1 = new Pen(Co­lor.Red, 10);
    p1.LineJoin = Li­ne­J­o­in.Be­vel;
    e.Graphics.DrawLines(p1, new Po­int[] { new Po­int(10, 10), new Po­int(100, 30), new Po­int(10, 60)});
    p1.LineJoin = Li­ne­J­o­in.Mi­ter;
    e.Graphics.DrawLines(p1, new Po­int[] { new Po­int(110, 10), new Po­int(200, 30), new Po­int(110, 60) });
    e.Graphics.SmoothingMode = Smo­ot­hin­g­Mo­de.Hig­h­Qu­ality;
    p1.LineJoin = Li­ne­J­o­in.Ro­und;
    e.Graphics.DrawLines(p1, new Po­int[] { new Po­int(10, 70), new Po­int(100, 100), new Po­int(10, 130) });
    p1.LineJoin = Li­ne­J­o­in.Mi­ter­C­lip­ped;
    p1.MiterLimit = 0.5f;
    e.Graphics.DrawLines(p1, new Po­int[] { new Po­int(110, 70), new Po­int(200, 100), new Po­int(110, 130) });
    
    
    Transform - мат­ри­ца пре­об­ра­зо­ва­ний. И есть еще на­бор стан­дар­т­ных фун­к­ций для тран­с­фор­ма­ций. Это мы рас­смот­рим в час­ти, пос­вя­щен­ной мат­рич­ным пре­об­ра­зо­ва­ни­ям.
    
Гла­ва 5. Кис­ти.
    
    Теперь рас­смот­рим кис­ти. Кис­тью мож­но за­ли­вать лю­бую зак­ры­тую фи­гу­ру, а так­же текст или то, что ри­су­ет ка­ран­даш. В от­ли­чии от ка­ран­да­шей­, кис­тей в .net мно­го (5), и ра­бо­тать с ни­ми при­хо­дит­ся по раз­но­му. Соб­с­т­вен­но сам класс Brush яв­ля­ет­ся ab­s­t­ract, так что им са­мим поль­зо­вать­ся нель­зя. Рас­смот­рим 5 по­рож­ден­ных от не­го клас­сов, го­то­вых к ис­поль­зо­ва­нию.
    
    Простые (сплош­ные) кис­ти
    Класс So­lid­B­rush.
    Самый прос­той ва­ри­ант - од­но­тон­ная кисть. Как и для ка­ран­да­шей­, су­щес­т­ву­ет пе­ре­чис­ле­ние од­но­тон­ных кис­тей - Brus­hes, опи­сы­ва­ющее од­но­тон­ные кис­ти всех из­вес­т­ных цве­тов, и пе­ре­чис­ле­ние System­B­rus­hes, опи­сы­ва­ющее кис­ти сис­тем­ных цве­тов. Ес­ли вам ну­жен нес­тан­дар­т­ный цвет - мож­но соз­дать свою кисть, пе­ре­дав цвет как ар­гу­мент в кон­с­т­рук­тор.
    
    private vo­id pa­nel1_Pa­int(obj­ect sen­der, Pa­in­tE­ven­tArgs e) {
    Brush br = Brus­hes.Red;
    e.Graphics.FillRectangle(br, 10, 10, 50, 50);
    br = System­B­rus­hes.But­ton­Hig­h­light;
    e.Graphics.FillRectangle(br, 70, 10, 50, 50);
    br = new So­lid­B­rush(Co­lor.Fro­mArgb(199, 157, 71));
    e.Graphics.FillRectangle(br, 130, 10, 50, 50);
    }
    
    Дальнейший код бу­ду пи­сать без име­ни фун­к­ции - он всег­да в pa­nel1_Pa­int.
    
    Кисти со штри­хов­кой
    Класс Hat­c­h­B­rush.
    Существует пе­ре­чис­ле­ние Hat­c­h­S­t­y­le, со­дер­жа­щее 54 из­вес­т­ных сис­те­ме штри­хов­ки. Со­от­вет­с­т­вен­но, класс Hat­c­h­B­rush поз­во­ля­ет соз­дать кисть с од­ной из этих 54 штри­хо­вок, и лю­бы­ми цве­та­ми фо­на и штри­хов­ки.
    
    Свойство клас­са Grap­hics Ren­de­rin­gO­ri­gin поз­во­ля­ет из­ме­нить по­ло­же­ние на­ча­ла штри­хов­ки.
    
    HatchBrush br = new Hat­c­h­B­rush(Hat­c­h­S­t­y­le.So­lid­Di­amond, Co­lor.Red, Co­lor.Whi­te);
    e.Graphics.FillRectangle(br, 10, 10, 50, 50);
    e.Graphics.RenderingOrigin = new Po­int(1, 1);
    e.Graphics.FillRectangle(br, 70, 10, 50, 50);
    br = new Hat­c­h­B­rush(Hat­c­h­S­t­y­le.Sphe­re, Co­lor.Whi­te, Co­lor.Blue);
    e.Graphics.FillRectangle(br, 130, 10, 50, 50);
    
    
    Текстурированная кисть
    Класс Tex­tu­reB­rush.
    Позволяет за­лить фор­му изоб­ра­же­ни­ем. Изоб­ра­же­ние пред­с­тав­ле­но клас­сом Bit­map, и мо­жет быть как соз­да­но прог­рам­мой­, так и заг­ру­же­но из фай­ла.
    Особое свой­ст­во - Wrap­Mo­de - опи­сы­ва­ет, как имен­но бу­дет за­пол­нять­ся фор­ма. Ва­ри­ан­ты: Clamp - изоб­ра­же­ние ри­су­ет­ся один раз, Ti­le - изоб­ра­же­ние раз­м­но­жа­ет­ся, что­бы за­пол­нить всю фор­му, Ti­leF­lipX - изоб­ра­же­ние от­ра­жа­ет­ся от­но­си­тель­но го­ри­зон­таль­ной оси, при каж­дом сле­ду­ющем ис­поль­зо­ва­нии, Ti­leF­lipY - ана­ло­гич­но, от­но­си­тель­но вер­ти­каль­ной оси, Ti­leF­lipXY - ана­ло­гич­но, от­но­си­тель­но го­ри­зон­таль­ной­, по­том вер­ти­каль­ной оси.
    
    TextureBrush br = new Tex­tu­reB­rush(Bit­map.From­Fi­le("smi­le.png"), Wrap­Mo­de.Clamp);
    e.Graphics.FillRectangle(br, 10, 10, 50, 50);
    br.WrapMode = Wrap­Mo­de.Ti­le;
    e.Graphics.FillRectangle(br, 70, 10, 50, 50);
    br.WrapMode = Wrap­Mo­de.Ti­leF­lipY;
    e.Graphics.FillRectangle(br, 130, 10, 50, 50);
    
    Обратите вни­ма­ние, за­лив­ка рас­счи­ты­ва­ет­ся от ле­во­го вер­х­не­го уг­ла кон­т­ро­ла (па­не­ли, в дан­ном слу­чае). Так что, ес­ли вы хо­ти­те, что­бы смай­лик влез це­ли­ком - вам на­до под­ви­нуть кисть, для это­го и при­ду­ма­ны ко­ор­ди­нат­ные пре­об­ра­зо­ва­ния. В дан­ном слу­чае, на­до бы до­пи­сать та­кую строч­ку пос­ле кон­с­т­рук­то­ра:
    
    br.TranslateTransform(9,10);
    
    Линейный гра­ди­ент
    Класс Li­ne­ar­G­ra­di­en­t­B­rush.
    В об­щем ви­де сис­те­ма та­кая - за­да­ют­ся точ­ки с ко­ор­ди­на­та­ми и цве­том, и сис­те­ма рас­счи­ты­ва­ет цве­та для всех ос­таль­ных пик­се­лей. Ес­ли ну­жен гра­ди­ен­т­ный пе­ре­ход от од­но­го цве­та к дру­го­му - то все прос­то, пря­мо в кон­с­т­рук­то­ре за­де­те две точ­ки, два цве­та и кисть го­то­ва. Вмес­то двух то­чек мо­же­те дать пря­мо­уголь­ник.
    Если ну­жен пе­ре­ход меж­ду нес­коль­ки­ми цве­та­ми - это чуть слож­нее. За­да­ете на­бор по­зи­ций и цве­тов в свой­ст­ве In­ter­po­la­ti­on­Co­lors.
    У этой кис­ти то­же есть свой­ст­во Wrap­Mo­de, ана­ло­гич­но пре­ды­ду­щей.
    Свойство Blend - поз­во­ля­ет за­дать па­ра­мет­ры про­пор­ций сме­ше­ния цве­тов, что­бы, нап­ри­мер, два цве­та сме­ши­ва­лись не рав­но­мер­но, а со сме­ще­ни­ем.
    Функции Set­Sig­ma­Bel­lSha­pe и Set­B­len­d­T­ri­an­gu­lar­S­ha­pe - поз­во­ля­ют ус­та­но­вить од­ну из двух схем рас­че­та гра­ди­ен­та. Вто­рая - по умол­ча­нию. Ар­гу­мен­ты фун­к­ций­: fo­cus - за­да­ет точ­ку (в до­лях еди­ни­цы) в ко­то­рой гра­ди­ент за­кан­чи­ва­ет­ся; sca­le - за­да­ет "ско­рость" из­ме­не­ния цве­та (1 - по умол­ча­нию).
    Свойство Li­ne­ar­Co­lors - поз­во­ля­ет ме­нять два край­них цве­та.
    Свойство Rec­tan­g­le - поз­во­ля­ет по­лу­чить пря­мо­уголь­ник, для ко­то­ро­го рас­счи­ты­ва­ет­ся гра­ди­ент.
    
    LinearGradientBrush br = new Li­ne­ar­G­ra­di­en­t­B­rush(new Po­int(10,10), new Po­int(60,60), Co­lor.Red, Co­lor.Blue);
    e.Graphics.FillRectangle(br, 10, 10, 50, 50);
    br = new Li­ne­ar­G­ra­di­en­t­B­rush(new Po­int(70, 10), new Po­int(120, 60), Co­lor.Red, Co­lor.Blue);
    Blend blnd = new Blend();
    blnd.Factors = new flo­at[] { 1f, 0.8f, 0.0f };
    blnd.Positions = new flo­at[] { 0f, 0.6f, 1f };
    br.Blend = blnd;
    e.Graphics.FillRectangle(br, 70, 10, 50, 50);
    br = new Li­ne­ar­G­ra­di­en­t­B­rush(new Po­int(130, 10), new Po­int(180, 60), Co­lor.Red, Co­lor.Blue);
    br.SetSigmaBellShape(1);
    e.Graphics.FillRectangle(br, 130, 10, 50, 50);
    
    Для мно­гоц­вет­ных гра­ди­ен­тов есть нес­коль­ко ог­ра­ни­че­ний - Blend не ра­бо­та­ет для In­ter­po­la­ti­on­Co­lors, Set­Sig­ma­Bel­lSha­pe и Set­B­len­d­T­ri­an­gu­lar­S­ha­pe - об­ну­ля­ют ус­та­нов­лен­ные In­ter­po­la­ti­on­Co­lors.
    
    LinearGradientBrush br = new Li­ne­ar­G­ra­di­en­t­B­rush(new Po­int(10,10), new Po­int(60,60), Co­lor.Red, Co­lor.Blue);
    ColorBlend in­t­Co­lors = new Co­lor­B­lend();
    intColors.Positions = new flo­at[] { 0f, 0.5f, 1f };
    intColors.Colors = new Co­lor[] { Co­lor.Red, Co­lor.Gre­en, Co­lor.Blue };
    br.InterpolationColors = in­t­Co­lors;
    e.Graphics.FillRectangle(br, 10, 10, 50, 50);
    br = new Li­ne­ar­G­ra­di­en­t­B­rush(new Po­int(70, 10), new Po­int(120, 60), Co­lor.Red, Co­lor.Blue);
    intColors = new Co­lor­B­lend();
    intColors.Positions = new flo­at[] { 0f, 0.2f, 0.4f, 0.6f, 0.8f, 1f };
    intColors.Colors = new Co­lor[] { Co­lor.Red, Co­lor.Oran­ge, Co­lor.Yel­low, Co­lor.Gre­en, Co­lor.Cyan, Co­lor.Blue };
    br.InterpolationColors = in­t­Co­lors;
    e.Graphics.FillRectangle(br, 70, 10, 50, 50);
    br = new Li­ne­ar­G­ra­di­en­t­B­rush(new Po­int(130, 10), new Po­int(180, 60), Co­lor.Red, Co­lor.Blue);
    br.SetSigmaBellShape(1, 0.7f);
    e.Graphics.FillRectangle(br, 130, 10, 50, 50);
    
    
    Градиенты слож­ной фор­мы
    Класс Pat­h­G­ra­di­en­t­B­rush.
    Этот класс поз­во­ля­ет соз­да­вать гра­ди­ен­ты слож­ной фор­мы. Кисть соз­да­ет­ся по Grap­hic­s­Path, ко­то­рый мо­жет быть лю­бой фор­мы. Од­на­ко, из опы­та, не ре­ко­мен­ду­ет­ся под­со­вы­вать очень слож­ные фор­мы с мно­ги­ми ра­зоб­щен­ны­ми кус­ка­ми. Кисть ис­хо­дит из од­ной зам­к­ну­той фор­мы.
    Для соз­да­ния гра­ди­ен­та в этом клас­се есть два пу­ти - пер­вый­, соз­да­ние мно­гоц­вет­но­го гра­ди­ен­та, рас­хо­дя­ще­го­ся из цен­т­раль­ной точ­ки и пов­то­ря­юще­го очер­та­ния фор­мы. Для это­го ис­поль­зу­ет­ся свой­ст­во In­ter­po­la­ti­on­Co­lors - для за­да­ния мас­си­ва цве­тов, и свой­ст­во Cen­ter­Po­int - для сме­ще­ния цен­т­раль­ной точ­ки, ес­ли на­до.
    Второй путь - прис­ва­ива­ние цве­та каж­до­му уг­лу фор­мы и цен­т­ру. Для это­го ис­поль­зу­ет­ся свой­ст­во Sur­ro­un­din­g­Co­lors для за­да­ния цве­тов в уг­лах, и свой­ст­во Cen­ter­Co­lor для за­да­ния цен­т­раль­но­го цве­та.
    У этой кис­ти так­же есть свой­ст­ва Blend и Wrap­Mo­de и фун­к­ции Set­Sig­ma­Bel­lSha­pe и Set­B­len­d­T­ri­an­gu­lar­S­ha­pe, ана­ло­гич­ные пре­ды­ду­щей.
    Свойство Fo­cus­S­ca­les поз­во­ля­ет рас­ши­рить зо­ну цве­та в цен­т­раль­ной точ­ке, зна­че­ния от 0 до 1, по умол­ча­нию - 0.
    
    GraphicsPath gp = new Grap­hic­s­Path();
    gp.AddRectangle(new Rec­tan­g­le(10, 10, 50, 50));
    PathGradientBrush br = new Pat­h­G­ra­di­en­t­B­rush(gp);
    ColorBlend in­t­Co­lors = new Co­lor­B­lend();
    intColors.Positions = new flo­at[] { 0f, 0.5f, 1f };
    intColors.Colors = new Co­lor[] { Co­lor.Red, Co­lor.Gre­en, Co­lor.Blue };
    br.InterpolationColors = in­t­Co­lors;
    e.Graphics.FillPath(br, gp);
    gp.Reset();
    gp.AddRectangle(new Rec­tan­g­le(70, 10, 50, 50));
    br = new Pat­h­G­ra­di­en­t­B­rush(gp);
    br.SurroundColors = new Co­lor[] {Co­lor.Red, Co­lor.Gre­en, Co­lor.Blue, Co­lor.Black };
    br.CenterColor = Co­lor.Whi­te;
    br.CenterPoint = new Po­intF(105, 35);
    e.Graphics.FillPath(br, gp);
    gp.Reset();
    gp.AddEllipse(130, 10, 50, 50);
    br = new Pat­h­G­ra­di­en­t­B­rush(gp);
    intColors = new Co­lor­B­lend();
    intColors.Positions = new flo­at[] { 0f, 0.2f, 0.4f, 0.6f, 0.8f, 1f };
    intColors.Colors = new Co­lor[] { Co­lor.Red, Co­lor.Oran­ge, Co­lor.Yel­low, Co­lor.Gre­en, Co­lor.Cyan, Co­lor.Blue };
    br.InterpolationColors = in­t­Co­lors;
    br.FocusScales = new Po­intF(0.3f, 0);
    e.Graphics.FillPath(br, gp);
    
    
Гла­ва 6. Ге­омет­ри­чес­кие эле­мен­ты.
    
    Рассмотрим те фи­гу­ры, эле­мен­ты и про­чие плос­кие объ­ек­ты, ко­то­ры­ми мож­но на­пол­нять свое изоб­ра­же­ние в GDI+.
    
    Простые фи­гу­ры
    Простых фи­гур все­го нес­коль­ко, но, как по­ка­зы­ва­ет прак­ти­ка, за­час­тую толь­ко они и нуж­ны. Рас­ска­зы­вать о них осо­бо не­че­го - все это есть в учеб­ни­ках ге­омет­рии :). К прос­тым фи­гу­рам от­но­сят­ся Пря­мо­уголь­ник (Rec­tan­g­le), Эл­липс (Ellip­se), Сек­тор (Pie). За­да­ют­ся они ко­ор­ди­на­та­ми вер­х­не­го-ле­во­го уг­ла и ши­ри­ной и вы­со­той. Для сек­то­ра ука­зы­ва­ет­ся еще угол на­ча­ла и ве­ли­чи­на уг­ла сек­то­ра. К чуть бо­лее слож­ным фи­гу­рам от­но­сят­ся По­ли­гон (Pol­y­gon) и Зам­к­ну­тая Кри­вая (Clo­sed Cur­ve). Эти за­да­ют­ся мас­си­вом уг­ло­вых то­чек. Для кри­вой до­пол­ни­тель­но ука­зы­ва­ет­ся кри­виз­на.
    
    Элементы
    Кроме го­то­вых фи­гур есть воз­мож­ность ри­со­вать эле­мен­ты фи­гур. Тут то­же все прос­то. Пря­мая (Li­ne), Кри­вая (Cur­ve), Ду­га (Arc) и Кри­вая Безье (Be­zi­er). Пря­мая за­да­ет­ся по двум точ­кам, кри­вая - по на­бо­ру то­чек и кри­виз­не, ду­га по пря­мо­уголь­ни­ку, опи­сы­ва­юще­му эл­липс, на­чаль­но­му уг­лу и уг­лу ду­ги, безь­ера - по 4 точ­кам. Ес­ли вдруг кто не зна­ет что та­кое безь­ера - па­ра ссы­лок:
    Тут по-англий­ски, но с при­ме­ром ко­да для пос­т­ро­ения - http://en.wikipedia.org/wiki/B%C3%A9zier_curve
    
    Пути
    Графические пу­ти (Grap­hics Path) - по су­ти, пред­с­тав­ля­ют со­бой на­бор зак­ры­тых фи­гур. В путь мож­но до­бав­лять це­ли­ком фи­гу­ры, стро­ки (текст) или сос­тав­лять фи­гу­ру из эле­мен­тов пря­мо в пу­ти. Для всех этих опе­ра­ций есть со­от­вет­с­т­ву­ющие фун­к­ции:
    AddEllipse - до­бав­ля­ет эл­липс и т.п.
    AddLine - до­бав­ля­ет пря­мую и т.п.
    Помните толь­ко об од­ном - ес­ли вы до­бав­ля­ете эле­мен­ты, они до­бав­ля­ют­ся к те­ку­щей фи­гу­ре. Ес­ли вы до­бав­ля­ете фи­гу­ру - она зак­ры­ва­ет те­ку­щую фи­гу­ру и до­бав­ля­ет­ся. Для уп­рав­ле­ния от­к­ры­ты­ми фи­гу­ра­ми есть две фун­к­ции:
    StartFigure - зак­ры­ва­ет те­ку­щую фи­гу­ру и от­к­ры­ва­ет но­вую. Все до­бав­ля­емые эле­мен­ты до­пи­сы­ва­ют­ся к но­вой фи­гу­ре.
    CloseFigure - зак­ры­ва­ет те­ку­щую фи­гу­ру.
    Обработка пу­ти про­ис­хо­дит быс­т­рее, чем ри­со­вать каж­дую фи­гу­ру от­дель­но. В за­ви­си­мос­ти от слож­нос­ти пу­ти, зна­че­ния пре­иму­щес­т­ва по­лу­ча­ют­ся раз­ные, но для слож­ных мно­го­ком­по­нен­т­ных пос­т­ро­ений - раз в 10 пу­ти быс­т­рее.
    
    Регионы
    Region - осо­бая фи­ча. Это, по су­ти, на­бор ак­тив­ных пик­се­лей. Со­от­вет­с­т­вен­но, ни­ка­ких фи­гур/эле­мен­тов не со­дер­жит, хо­тя и мо­жет быть соз­дан из пря­мо­уголь­ни­ка или пу­ти. Ос­нов­ное ис­поль­зо­ва­ние - уп­рав­ле­ние би­то­вой проз­рач­нос­тью. Я, нап­ри­мер, час­то ис­поль­зую на­бор ре­ги­онов для слож­ных мно­гос­лой­ных ри­со­ва­ний­, что­бы за­ра­нее оп­ре­де­лить об­ласть ри­со­ва­ния для каж­до­го слоя - это эко­но­мит ре­сур­сы при про­ри­сов­ке, и га­ран­ти­ру­ет, что за­лив­ка од­но­го слоя не вы­ле­зет на дру­гой. Об­ра­бот­ка ре­ги­онов про­ис­хо­дит зна­чи­тель­но быс­т­рее, чем об­ра­бот­ка пу­тей. Сов­сем не­дав­но срав­нил - про­ри­сов­ка слож­ной кар­тин­ки, сос­то­ящей из нес­коль­ких со­тен зам­к­ну­тых кри­вых слож­ной фор­мы; сна­ча­ла бы­ли соз­да­ны кри­вые, по­том они вво­ди­лись в гра­фи­чес­кий путь, а по­том ли­бо ри­со­ва­лись из пу­ти, ли­бо соз­да­вал­ся ре­ги­он из пу­ти, и за­ли­вал­ся он. За­лив­ка ре­ги­она за­ни­ма­ла в, при­мер­но, 1000 раз мень­ше вре­ме­ни.
    
    Текст
    Рисование тек­с­та вы­пол­ня­ет­ся че­рез фун­к­цию Draw­S­t­ring. В ка­чес­т­ве ар­гу­мен­тов пе­ре­да­ют­ся стро­ка, шрифт, кисть для за­лив­ки и по­ло­же­ние. Воз­мож­ные ва­ри­ан­ты - мож­но пе­ре­да­вать ко­ор­ди­на­ты вер­х­не­го-ле­во­го уг­ла, а мож­но пе­ре­да­вать пря­мо­уголь­ник, в ко­то­рый на­до по­мес­тить текст, тог­да текст не бу­дет пре­вы­шать пря­мо­уголь­ник по ши­ри­не.
    Шрифт мож­но, как обыч­но, взять из пе­ре­чис­ле­ния System­Fonts, а мож­но соз­дать са­мим. При соз­да­нии шриф­та на­до ука­зы­вать се­мей­ст­во и раз­мер, а так­же мож­но уточ­нить - в чем имен­но вы ука­за­ли раз­мер, оп­ре­де­лить до­пол­ни­тель­ные сти­ли шриф­та (кур­сив, нап­ри­мер), мож­но ука­зать на­бор сим­во­лов GDI, ко­то­рый на­до ис­поль­зо­вать, а так­же сде­лать шрифт вер­ти­каль­ным. Са­мое глав­ное тут - се­мей­ст­во шриф­та, его мож­но ука­зать че­рез стро­ко­вое наз­ва­ние, а мож­но выб­рать из мас­си­ва Fon­t­Fa­mily.Fa­mi­li­es.
    Еще од­но - час­то бы­ва­ет на­до оп­ре­де­лить раз­ме­ры тек­с­та, ког­да он бу­дет на­ри­со­ван. Для это­го есть две фун­к­ции в клас­се Grap­hics: Me­asu­reS­t­ring и Me­asu­reC­ha­rac­ter­Ran­ges. Пер­вая вы­чис­ля­ет пря­мо­уголь­ник (струк­ту­ру Si­zeF) для дан­ных стро­ки и шриф­та. При не­об­хо­ди­мос­ти, этой фун­к­ции мож­но то­же ука­зать пря­мо­уголь­ник, в ко­то­ром дол­жен быть раз­ме­щен текст, тог­да ши­ри­на сох­ра­нить­ся, а не­об­хо­ди­мую вы­со­ту вы­чис­лят. Me­asu­reC­ha­rac­ter­Ran­ges воз­в­ра­ща­ет мас­сив Re­gi­on'ов. Рас­чет за­ни­ма­емо­го прос­т­ран­с­т­ва про­во­дить­ся по-раз­но­му, и ес­ли ва­ша прог­рам­ма дол­ж­на быть очень точ­на в раз­ме­ще­нии тек­с­та на эк­ра­не/прин­те­ре и пр., то вам при­дет­ся ис­поль­зо­вать обе фун­к­ции. Но в обыч­ных прог­рам­мах нуж­на толь­ко пер­вая. Она удоб­нее для ис­поль­зо­ва­ния, быс­т­рее ра­бо­та­ет, а то, что она бы­ва­ет не слиш­ком точ­на - так это или поч­ти не за­мет­но, или лег­ко ком­пен­си­ро­вать. Она ес­ли и оши­ба­ет­ся, то толь­ко в мень­шую сто­ро­ну, а зна­чит, дос­та­точ­но пре­дус­мот­реть за­пас прос­т­ран­с­т­ва в 3-4 пик­се­ля, и все бу­дет в по­ряд­ке.
    
Гла­ва 7. Мат­рич­ные тран­с­фор­ма­ции.
    
    Многое из ни­же­на­пи­сан­но­го яв­ля­ет­ся воль­ным пе­ре­во­дом/пе­рес­ка­зом ма­те­ри­алов с это­го сай­та - http://www.bobpowell.net/.
    
    Системы ко­ор­ди­нат
    Если ко­рот­ко - в GDI+ су­щес­т­ву­ет на­бор двух­мер­ных ко­ор­ди­нат­ных сис­тем. Ось X нап­рав­ле­на сле­ва-нап­ра­во, ось Y свер­ху-вниз. Ко­ор­ди­нат­ных сис­тем три:
    World co­or­di­na­te spa­ce - ми­ро­вая сис­те­ма ко­ор­ди­нат. Та сис­те­ма, в ко­то­рой вы ра­бо­та­ете. Еди­ни­цы из­ме­ре­ния - дроб­ные чис­ла, ма­ло что оз­на­ча­ющие для всех, кро­ме вас :).
    Page Co­or­di­na­te Spa­ce - сис­те­ма ко­ор­ди­нат стра­ни­цы. Еди­ни­ца­ми из­ме­ре­ния мо­гут яв­лять­ся раз­ные ве­ли­чи­ны - пик­се­ли, дюй­мы, мил­ли­мет­ры и пр. Вы ус­та­нав­ли­ва­ете ко­эф­фи­ци­ент пе­ре­хо­да меж­ду чис­ла­ми из ми­ро­вой сис­те­мы и сис­те­мой ко­ор­ди­нат стра­ни­цы че­рез па­ра­метр Pa­geS­ca­le объ­ек­та Grap­hics. Так ва­ши чис­ла прев­ра­ща­ют­ся в пик­се­ли, дюй­мы и пр.
    Device Co­or­di­na­te Spa­ce - сис­те­ма ко­ор­ди­нат ус­т­рой­ст­ва. Это не­дос­туп­ная прог­рам­мис­там об­ласть, кон­т­ро­ли­ру­ет­ся сис­те­мой и драй­ве­ра­ми про­из­во­ди­те­лей обо­ру­до­ва­ния. На этом уров­не дюй­мы и пр. прев­ра­ща­ют­ся в ре­аль­ные точ­ки - зер­на эк­ра­на, точ­ки (кап­ли) крас­ки и т.д.
    
    В сум­ме, на­бор та­ких сис­тем ко­ор­ди­нат поз­во­ля­ет го­во­рить о GDI+ как о "не­за­ви­си­мой от раз­ре­ше­ния" сис­те­ме ри­со­ва­ния. За­дан­ная тол­щи­на в мил­ли­метр бу­дет мил­ли­мет­ром как на мо­ни­то­ре, так и на прин­те­ре, так и на плот­те­ре и т.д. Ну, это в иде­але.
    
    Единицы из­ме­ре­ния сис­те­мы ко­ор­ди­нат стра­ни­цы:
    Pixel - пик­сель, он и в Аф­ри­ке пик­сель. Но на­до пом­нить, что раз­мер пик­се­ля раз­ный­, на раз­ных ус­т­рой­ст­вах. Т.е. ваш ри­су­нок мо­жет выг­ля­деть по-раз­но­му на эк­ра­не и на прин­те­ре.
    Millimeter - мил­ли­метр. Выг­ля­дит вез­де оди­на­ко­во.
    Inch - дюйм. Выг­ля­дит вез­де оди­на­ко­во.
    Point - по­инт или точ­ка. 1/72 дюй­ма. Еди­ни­ца из­ме­ре­ния шриф­та в ти­пог­ра­фи­ях. Выг­ля­дит вез­де оди­на­ко­во.
    Display - 1/75 дюй­ма. Ког­да-то - ве­ли­чи­на зер­на ЭЛТ мо­ни­то­ров. Выг­ля­дит вез­де оди­на­ко­во.
    Document - 1/300 дюй­ма. Стан­дар­т­ное раз­ре­ше­ние ла­зер­ных прин­те­ров. Оди­на­ко­вость не га­ран­ти­ру­ет­ся.
    World - дол­ж­но быть то же, что и пик­сель. Но на прак­ти­ке вы­да­ет ку­чу оши­бок - луч­ше не ис­поль­зо­вать.
    
    Матричные тран­с­фор­ма­ции
    Зачем они во­об­ще нуж­ны - за­тем, что­бы не пи­сать вот та­кие вот длин­ные строч­ки:
    
    DrawLine(myPen,(panX+x1)*zoom,(panY+y1)*zoom,(panX+x2)*zoom,(panY+y2)*zoom);
    Умножение на ко­эф­фи­ци­ент уве­ли­че­ния - 4 лиш­ние опе­ра­ции, да и чи­та­емость ко­да рез­ко ухуд­ша­ет­ся.
    
    Все уп­рав­ле­ние тран­с­фор­ма­ци­ями ве­дет­ся че­рез свой­ст­во Tran­s­form (класс Mat­rix), объ­ек­та Grap­hics.
    С тран­с­фор­ма­ци­ями все до­воль­но прос­то, до тех пор, по­ка тран­с­фор­ма­ции прос­тые. У клас­са Grap­hics есть вспо­мо­га­тель­ные фун­к­ции - Tran­s­la­teT­ran­s­form и пр. для про­ве­де­ния прос­тых тран­с­фор­ма­ций.
    TranslateTransform - пе­ре­мес­тить.
    ScaleTransform - мас­ш­та­би­ро­вать.
    RotateTransform - по­вер­нуть.
    ResetTransform - сбро­сить все тран­с­фор­ма­ции.
    Все тран­с­фор­ма­ции вы­пол­ня­ют­ся от­но­си­тель­но на­ча­ла ко­ор­ди­нат.
    
    У клас­са Mat­rix есть ана­ло­гич­ный на­бор фун­к­ций.
    Translate - пе­ре­мес­тить.
    Scale - мас­ш­та­би­ро­вать.
    Rotate - по­вер­нуть от­но­си­тель­но на­ча­ла ко­ор­ди­нат.
    RotateAt - по­вер­нуть от­но­си­тель­но точ­ки.
    Shear - ис­ка­зить (сде­лать па­рал­ле­лог­рам из пря­мо­уголь­ни­ка).
    Reset - сбро­сить тран­с­фор­ма­ции.
    Все эти фун­к­ции, в ва­ри­ан­те для клас­са Mat­rix име­ют ар­гу­мент, поз­во­ля­ющий за­дать по­ря­док при­ме­не­ния тран­с­фор­ма­ций. Mat­ri­xOr­der.Append - до­ба­вить тран­с­фор­ма­цию в ко­нец, Mat­ri­xOr­der.Pre­pend - пос­та­вить тран­с­фор­ма­цию в на­ча­ло.
    
    Если нуж­но что-то пос­лож­нее, при­дет­ся вспом­нить, как ра­бо­тать с мат­ри­ца­ми.
    Итак, в ос­но­ве ле­жит мат­ри­ца 2х3.
    m11,m12
    m21,m22
    dx nbsp;,dy
    В даль­ней­шем, мат­ри­цу бу­ду за­пи­сы­вать в стро­ку - (m11,m12,m21,m22,dx,dy).
    Итак, мат­ри­ца без тран­с­фор­ма­ций выг­ля­дит так: (1,0,0,1,0,0)
    
    Каждая точ­ка, пе­ред тем, как бу­дет на­ри­со­ва­на, ум­но­жа­ет­ся на мат­ри­цу тран­с­фор­ма­ций.
    x = x1*m11+y1*m12+dx
    y = x1*m21+y1*m22+dy
    
    В об­щем-то, ос­нов­ная суть уже ска­за­на :). Те­перь ба­зо­вые мат­ри­цы, для стан­дар­т­ных тран­с­фор­ма­ций­:
    вдвое боль­ше - (2,0,0,2,0,0)
    вдвое мень­ше - (0.5,0,0,0.5,0,0)
    перенести впра­во на 10 и вниз на 5 - (1,0,0,1,10,5)
    матрица по­во­ро­та в об­щем ви­де - (co­sA, si­nA, -si­nA, co­sA, 0, 0).
    например, по­во­рот на 30 гра­ду­сов по ча­со­вой стрел­ке: (0.866, 0.5, -0.5, 0.866, 0, 0)
    переворот оси Y - (1,0,0,-1,0,0)
    
    Примеры ис­поль­зо­ва­ния:
    увеличить вдвое
    
    private vo­id pa­nel1_Pa­int(obj­ect sen­der, Pa­in­tE­ven­tArgs e) {
    e.Graphics.FillEllipse(Brushes.Blue, 20, 30, 30, 20);
    e.Graphics.Transform = new Mat­rix(2, 0, 0, 2, 0, 0);
    e.Graphics.FillEllipse(Brushes.Blue, 20, 30, 30, 20);
    }
    
    Того же эф­фек­та мож­но дос­тичь, ис­поль­зуя
    
    e.Graphics.ScaleTransform(2,2);
    
    переворот оси Y
    
    private vo­id pa­nel1_Pa­int(obj­ect sen­der, Pa­in­tE­ven­tArgs e) {
    e.Graphics.DrawString("строка", new Font("Ti­mes New Ro­man", 24), Brus­hes.Black, 10, 10);
    e.Graphics.Transform = new Mat­rix(1, 0, 0, -1, 0, 120);
    e.Graphics.DrawString("строка", new Font("Ti­mes New Ro­man", 24), Brus­hes.Black, 10, 10);
    }
    
    Обратите вни­ма­ние, пе­ре­во­ра­чи­вая ось Y, вы рас­по­ла­га­ете об­ласть ри­со­ва­ния за вер­х­ним кра­ем сво­его кон­т­ро­ла, по­это­му не­об­хо­ди­мо еще и пе­ре­ме­щать ее по оси Y.
    
Гла­ва 8. Гра­фи­чес­кие фай­лы.
    
    Еще раз на­по­ми­наю - все ри­со­ва­ние про­из­во­дит­ся в объ­ек­те клас­са Grap­hics, сле­до­ва­тель­но - ре­зуль­тат ри­со­ва­ния ос­та­ет­ся в том объ­ек­те, от ко­то­ро­го сде­лан объ­ект клас­са Grap­hics.
    
    Класс Bit­map
    В об­щем вид этот класс сде­лан для хра­не­ния рас­т­ро­вых изоб­ра­же­ний. Вы мо­же­те соз­дать объ­ект клас­са Bit­map, на­ри­со­вать в нем что-то, по­том это что-то по­ка­зать поль­зо­ва­те­лю, рас­пе­ча­тать, сох­ра­нить в файл и т.д.
    Реально же та­кой под­ход ред­ко прак­ти­ку­ет­ся. Ес­ли на­до что-то по­ка­зать поль­зо­ва­те­лю - это обыч­но ри­су­ет­ся в кон­т­ро­ле, так как ста­тич­ные изоб­ра­же­ния нуж­ны ред­ко, а ди­на­ми­чес­кие про­ще ри­со­вать каж­дый раз сра­зу в кон­т­ро­ле, чем ри­со­вать в па­мя­ти в Bit­map, а по­том вы­во­дить этот Bit­map в кон­т­рол. И про­чее так же - обыч­но ис­поль­зу­ет­ся еди­ный код ри­со­ва­ния, ко­то­рый мо­жет под­с­т­ра­ивать­ся под тип вы­во­да (экран, прин­тер, файл), а соб­с­т­вен­но класс Bit­map ис­поль­зу­ет­ся толь­ко для заг­руз­ки/сох­ра­не­ния фай­лов.
    
    Долго тут рас­ска­зы­вать, на мой взгляд, не­че­го. Рас­смот­рим код ре­ше­ния од­ной­, но очень рас­п­рос­т­ра­нен­ной­, за­да­чи. Тре­бу­ет­ся:
    1. заг­ру­зить изоб­ра­же­ние од­но­го из "род­ных" для .NET фор­ма­тов (jpeg, png, tiff, gif, bmp)
    2. из­ме­нить изоб­ра­же­ние сог­лас­но по­же­ла­ни­ям поль­зо­ва­те­ля (изме­нить раз­мер/на­ло­жить ко­пи­рай­т)
    3. сох­ра­нить в один из род­ных фор­ма­тов, с ис­поль­зо­ва­ни­ем па­ра­мет­ров ко­ди­ро­ва­ния
    
    //параметры, вво­ди­мые поль­зо­ва­те­лем
    string in­put_fi­le­na­me = "input.jpg";
    string out­put_fi­le­na­me = "out­put";
    ImageFormat imft = Ima­ge­For­mat.Jpeg;
    Size new_ima­ge_si­ze = Si­ze.Empty;
    new_image_size = new Si­ze(640, 480);
    string cop­y­right = "(c) 2006 do­ci.nnm.ru/sel­f­p­rog­ram­mer";
    long com­p­ress = 80;
    
    Bitmap bmp_in = null;
    Bitmap bmp_out;
    if (!new_ima­ge_si­ze.IsEmpty) { //если но­вый раз­мер вве­ден
    bmp_in = (Bit­map)Bit­map.From­Fi­le(input_fi­le­na­me); // счи­ты­ва­ем ори­ги­нал
    bmp_out = new Bit­map(bmp_in, new_ima­ge_si­ze); // соз­да­ем ко­пию нуж­но­го раз­ме­ра
    }
    else { // ес­ли ме­нять раз­мер не на­до
    bmp_out = (Bit­map)Bit­map.From­Fi­le(input_fi­le­na­me); // счи­ты­ва­ем ори­ги­нал
    }
    if (!String.IsNul­lO­rEm­p­ty(cop­y­right)) {
    // ри­су­ем ко­пи­рай­т
    Graphics gr = Grap­hics.Fro­mI­ma­ge(bmp_out);
    gr.DrawString(copyright, System­Fon­ts.Cap­ti­on­Font, Brus­hes.Whi­te, new_ima­ge_si­ze.Width - 220, new_ima­ge_si­ze.He­ight - 30);
    gr.Dispose();
    }
    // сох­ра­ня­ем
    ImageCodecInfo im­co­dec = null;
    EncoderParameters en­c­pa­rams = null;
    if (imft == Ima­ge­For­mat.Jpeg) { // ес­ли jpeg - с па­ра­мет­ра­ми
    imcodec = Ge­tEn­co­de­rIn­fo("ima­ge/jpeg");
    encparams = new En­co­der­Pa­ra­me­ters(1);
    encparams.Param[0] = new En­co­der­Pa­ra­me­ter(Enco­der.Qu­ality, com­p­ress);
    bmp_out.Save(output_filename + ".jpg", im­co­dec, en­c­pa­rams);
    }
    else if (imft == Ima­ge­For­mat.Png) { // ес­ли png - по умол­ча­нию
    bmp_out.Save(output_filename, imft);
    }
    if (bmp_in != null) {
    bmp_in.Dispose();
    }
    bmp_out.Dispose();
    Параметры вво­ди­мые поль­зо­ва­те­лем - на­до бы по­лу­чать из ин­тер­фей­са, но мне лень бы­ло тут еще кноп­ки во­ро­тить.
    
    По по­во­ду сох­ра­не­ния в раз­ных фор­ма­тах, я рас­смот­рел толь­ко 2. По об­ра­зу и по­до­бию мож­но до­ба­вить и дру­гие. Пом­ни­те о та­ких ве­щах - нор­маль­ная под­дер­ж­ка фор­ма­тов в .NET есть толь­ко для jpeg и bmp. Для Tiff - сжа­тие LZW не под­дер­жи­ва­ет­ся, хо­тя за­яв­ле­но, для Png - нет под­дер­ж­ки ка­чес­т­ва, хо­тя за­яв­ле­на. Так что, при­хо­дить­ся сох­ра­нять по де­фол­ту.
    В jpeg у ме­ня сох­ра­ня­ет­ся с ис­поль­зо­ва­ни­ем En­co­der'a, что не­обя­за­тель­но - в jpeg мож­но сох­ра­нять так же как у ме­ня в png, ес­ли вам не ме­ша­ет, что ка­чес­т­во бу­дет вы­би­рать­ся са­мо.
    
    Для из­ме­не­ния раз­ме­ра нуж­но соз­да­вать но­вый Bit­map, при­чем ес­ли нуж­но хо­ро­шее ка­чес­т­во, то на­до соз­да­вать но­вый Bit­map нуж­но­го раз­ме­ра, соз­да­вать для не­го Grap­hics, за­да­вать Grap­hics на­илуч­шее ка­чес­т­во сгла­жи­ва­ния/интер­по­ля­ции и ри­со­вать ста­рый Bit­map в но­вом че­рез Dra­wI­ma­ge.
    Другие из­ме­не­ния мож­но про­во­дить пря­мо над заг­ру­жен­ным Bit­map, не соз­да­вая но­во­го.
    
    класс Me­ta­fi­le
    Не знаю, как он дол­жен ис­поль­зо­вать­ся - со сво­ими ме­та-воз­мож­нос­тя­ми, но я ис­поль­зо­вал, и ви­дел ис­поль­зо­ва­ние, толь­ко в ка­чес­т­ве фай­ла век­тор­ной гра­фи­ки.
    Есть не­ко­то­рый глюк в сис­те­ме ис­поль­зо­ва­ния ме­та­фай­лов, но об­щий прин­цип та­кой же - соз­да­ете ме­та­фай­л, соз­да­ете из не­го Grap­hics, ри­су­ете, уби­ва­ете. От­ли­чия от рас­т­ра - при ри­со­ва­нии все сра­зу пи­шет­ся в файл, ис­поль­зо­вать sa­ve не на­до.
    Пример ко­да:
    
    string out­put_fi­le­na­me = "out­put.emf";
    EmfType em­f­t­y­pe = Em­f­T­y­pe.EmfPlus­Du­al;
    
    Bitmap tmpbmp = new Bit­map(800, 600);
    tmpbmp.SetResolution(300, 300);
    Graphics gr1 = Grap­hics.Fro­mI­ma­ge(tmpbmp);
    IntPtr hdc = gr1.GetHdc();
    Metafile vec­tor_ima­ge = new Me­ta­fi­le(out­put_fi­le­na­me, hdc, em­f­t­y­pe);
    Graphics tgr = Grap­hics.Fro­mI­ma­ge(vec­tor_ima­ge);
    tgr.Clear(Color.White);
    
    Rectangle rect1 = new Rec­tan­g­le(10, 10, 150, 150);
    tgr.FillRectangle(Brushes.Blue, rect1);
    tgr.DrawRectangle(Pens.Red, rect1);
    tgr.DrawString("some text", System­Fon­ts.Cap­ti­on­Font, Brus­hes.Black, 200, 200);
    
    tgr.Dispose();
    vector_image.Dispose();
    gr1.Dispose();
    tmpbmp.Dispose();
    emftype - тип emf фай­ла, мо­жет быть Emf, Em­f­P­lus и Em­f­P­lus­Du­al. Со­от­вет­с­т­вен­но, emf и em­f­P­lus - раз­ные вер­сии фор­ма­та, Em­f­P­lus­Du­al - в файл каж­дая за­пись пи­шет­ся в двух фор­ма­тах сра­зу, и в emf и в em­f­P­lus.
    
    Как ви­ди­те, для соз­да­ния ме­та­фай­ла ну­жен ука­за­тель на кон­текст ус­т­рой­ст­ва (Hdc), для по­лу­че­ния ко­то­ро­го и го­ро­дит­ся весь ого­род с соз­да­ни­ем вре­мен­но­го bit­map и соз­да­ни­ем Grap­hics из не­го - это поз­во­ля­ет опи­сать па­ра­мет­ры ри­со­ва­ния (раз­ре­ше­ния, сис­те­му ко­ор­ди­нат) для соз­да­ния hdc и пе­ре­да­чи его ме­та­фай­лу.
    
    После ис­пол­не­ния ко­да мо­же­те поп­ро­бо­вать от­к­рыть файл в лю­бом ре­дак­то­ре век­тор­ной гра­фи­ки и пос­мот­реть что по­лу­чи­лось. Не­ко­то­рые из­вес­т­ные мо­мен­ты: Co­rel Draw вер­сии до 12 от­к­ры­ва­ет emf в сгруп­пи­ро­ван­ном ви­де - не за­будь­те сна­ча­ла дать un­g­ro­up, по­том уже смот­реть по-объек­т­но. Тот же Co­rel до пос­лед­ней вер­сии неп­ра­виль­но счи­ты­ва­ет еди­ни­цы из­ме­ре­ния тек­с­та - в нем текст всег­да отоб­ра­жа­ет­ся очень-очень мел­ко и со сме­щен­ны­ми ко­ор­ди­на­та­ми.
    
Глава 9. Примеры.
    
    Первый при­мер
    Создаем фор­му с па­нель­кой­, на ко­то­рой ри­су­ем вся­кое раз­ное (мно­го­ком­по­нен­т­ный путь и текст в рам­ке), под­к­лю­ча­ем ав­тос­к­рол­линг и мас­ш­та­би­ро­ва­ние: ле­вая кноп­ка - приб­ли­зить, пра­вая - от­да­лить.
    Конструктор фор­мы, в нем за­да­ем путь, ко­то­рый бу­дем ри­со­вать в пер­вой па­не­ли и соз­да­ем мат­ри­цу мас­ш­та­би­ро­ва­ния.
    
    public Form1() {
    InitializeComponent();
    gp = new Grap­hic­s­Path();
    gp.StartFigure();
    gp.AddLine(5, 5, 50, 50);
    gp.AddBezier(55, 55, 65, 35, 75, 35, 85, 55);
    gp.CloseFigure();
    gp.AddEllipse(5, 100, 70, 70);
    gp.AddString("(c) 2006 do­ci.nnm.ru/sel­f­p­rog­ram­mer", Fon­t­Fa­mily.Ge­ne­ric­San­s­Se­rif, 0, 12, new Po­int(5, 180), Strin­g­For­mat.Ge­ne­ric­De­fa­ult);
    zoomMatrix = new Mat­rix(); // мат­ри­ца мас­ш­та­ба
    }
    Глобальные пе­ре­мен­ные - путь, мат­ри­ца мас­ш­та­ба, ко­эф­фи­ци­ент мас­ш­та­би­ро­ва­ния.
    
    private Grap­hic­s­Path gp;
    private Mat­rix zo­om­Mat­rix;
    private flo­at _zo­om = 2;
    private flo­at zo­om {
    get { re­turn _zo­om; }
    set {
    _zoom = va­lue;
    zoomMatrix.Reset();
    zoomMatrix.Scale(value, va­lue);
    RectangleF rect = gp.Get­Bo­un­ds(zo­om­Mat­rix); // по­лу­ча­ем раз­ме­ры пу­ти пос­ле из­ме­не­ния мас­ш­та­ба
    panel1.AutoScrollMinSize = new Si­ze((int)rect.Width, (int)rect.He­ight); // ус­та­нав­ли­ва­ем об­ласть скрол­лин­га
    panel1.Invalidate(); // об­нов­ля­ем па­нель­ку
    }
    }
    private vo­id pa­nel1_Pa­int(obj­ect sen­der, Pa­in­tE­ven­tArgs e) {
    e.Graphics.Transform = zo­om­Mat­rix; // ус­та­нав­ли­ва­ем те­ку­щий мас­ш­таб
    e.Graphics.TranslateTransform(panel1.AutoScrollPosition.X, pa­nel1.AutoS­c­rol­lPo­si­ti­on.Y); // и те­ку­щее сме­ще­ние
    e.Graphics.FillPath(Brushes.Aqua, gp); // ри­су­ем путь
    Font ft = new Font("Ti­mes New Ro­man", 16); // соз­да­ем шрифт
    SizeF sz = e.Grap­hics.Me­asu­reS­t­ring("Текст в рам­ке", ft); // за­ме­ря­ем строч­ку тек­с­та
    e.Graphics.DrawString("Текст в рам­ке", ft, Brus­hes.Navy, 5, 200); // ри­су­ем текст
    e.Graphics.DrawRectangle(Pens.Navy, 5, 200, sz.Width, sz.He­ight); // ри­су­ем рам­ку
    }
    Осталось толь­ко из­ме­не­ние мас­ш­та­ба на кноп­ку мы­ши пос­та­вить:
    
    private vo­id pa­nel1_Mo­use­Up(obj­ect sen­der, Mo­use­Even­tArgs e) {
    if (e.But­ton == Mo­use­But­tons.Left) {
    zoom += 0.2f;
    }
    else if (e.But­ton == Mo­use­But­tons.Right) {
    zoom -= 0.2f;
    }
    }
    Готово - мо­же­те про­ве­рять. Дол­ж­но по­лу­чить­ся что-то вро­де та­ко­го:
    
    
    Второй при­мер
    Нарисуем пря­мо­уголь­ник со скруг­лен­ны­ми уг­ла­ми, на­пи­шем что-ни­будь по од­ной сто­ро­не и пусть он у нас сво­ра­чи­ва­ет­ся-раз­во­ра­чи­ва­ет­ся по кли­ку.
    Сначала на­ри­су­ем:
    
    private vo­id pa­nel2_Pa­int(obj­ect sen­der, Pa­in­tE­ven­tArgs e) {
    e.Graphics.FillRectangle(Brushes.Violet, e.Clip­Rec­tan­g­le);
    int X = 5, Y = 5, he­ight = 150, ra­di­us = 10;
    int width = ro­un­d­Rec­t­Width;
    GraphicsPath gp2 = new Grap­hic­s­Path();
    gp2.AddLine(X + ra­di­us, Y, X + width - (ra­di­us * 2), Y);
    gp2.AddArc(X + width - (ra­di­us * 2), Y, ra­di­us * 2, ra­di­us * 2, 270, 90);
    gp2.AddLine(X + width, Y + ra­di­us, X + width, Y + he­ight - (ra­di­us * 2));
    gp2.AddArc(X + width - (ra­di­us * 2), Y + he­ight - (ra­di­us * 2), ra­di­us * 2, ra­di­us * 2, 0, 90);
    gp2.AddLine(X + width - (ra­di­us * 2), Y + he­ight, X + ra­di­us, Y + he­ight);
    gp2.AddArc(X, Y + he­ight - (ra­di­us * 2), ra­di­us * 2, ra­di­us * 2, 90, 90);
    gp2.AddLine(X, Y + he­ight - (ra­di­us * 2), X, Y + ra­di­us);
    gp2.AddArc(X, Y, ra­di­us * 2, ra­di­us * 2, 180, 90);
    gp2.CloseFigure();
    e.Graphics.FillPath(Brushes.Aqua, gp2);
    e.Graphics.DrawPath(Pens.Navy, gp2);
    gp2.Dispose();
    StringFormat sf = new Strin­g­For­mat(Strin­g­For­mat­F­lags.Di­rec­ti­on­Ver­ti­cal);
    sf.Alignment = Strin­gA­lig­n­ment.Cen­ter;
    sf.LineAlignment = Strin­gA­lig­n­ment.Cen­ter;
    e.Graphics.DrawString("Sample", new Font("Ti­mes New Ro­man", 18, Fon­t­S­t­y­le.Under­li­ne, Grap­hic­sU­nit.Pi­xel), Brus­hes.Black, new Rec­tan­g­leF(5, 7, 20, 150), sf);
    e.Graphics.DrawLine(Pens.Navy, 25, 5, 25, 155);
    }
    Код ри­со­ва­ния та­ких пря­мо­уголь­ни­ков я чес­т­но(с)ты­рил с Bob Po­well.
    Добавим эти две строч­ки в кон­с­т­рук­тор фор­мы:
    
    roundRectWidth = 150;
    roundRectDir = true;
    И со­от­вет­с­т­вен­ные гло­баль­ные пе­ре­мен­ные объ­явим:
    
    private int ro­un­d­Rec­t­Width;
    private bo­ol ro­un­d­Rec­t­Dir;
    
    Теперь за­ря­дим ани­ма­цию:
    Добавим в фор­му ком­по­нент ti­mer, ус­та­но­вим ему in­ter­val=20 и про­пи­шем та­кую фун­к­цию на tick:
    
    private vo­id ti­mer1_Tick(obj­ect sen­der, Even­tArgs e) {
    if (ro­un­d­Rec­t­Dir) {
    roundRectWidth -= 2;
    if (ro­un­d­Rec­t­Width ‹= 27) { ti­mer1.Stop();
    roundRectDir = fal­se;
    }
    }
    else {
    roundRectWidth += 2;
    if (ro­un­d­Rec­t­Width ›= 150) {
    timer1.Stop();
    roundRectDir = true;
    }
    }
    panel2.Invalidate(new Rec­tan­g­le(ro­un­d­Rec­t­Width-3, 0, 11, 160));
    }
    Думаю, все яс­но - ес­ли од­но нап­рав­ле­ние - умень­ша­ем ши­ри­ну пря­мо­уголь­ни­ка, по­ка не кон­чит­ся, тог­да вык­лю­ча­ем тай­мер и ме­ня­ем нап­рав­ле­ние. Ес­ли дру­гое нап­рав­ле­ние - уве­ли­чи­ва­ем ши­ри­ну, по­ка не вы­рас­тет, как на­до, тог­да ос­та­нав­ли­ва­ем тай­мер и ме­ня­ем нап­рав­ле­ние.
    Последняя строч­ка - пе­ре­ри­сов­ка па­не­ли. Ес­ли выз­вать In­va­li­da­te без ар­гу­мен­тов, бу­дет пе­ре­ри­со­вы­вать­ся вся па­нель, и бу­дет она пос­то­ян­но мер­цать. Что­бы не мер­ца­ла - пе­ре­ри­со­вы­ва­ем толь­ко из­ме­нив­шу­юся часть - пря­мо­уголь­ник из­ме­не­ний. Все рав­но мер­цать бу­дет, но мень­ше и ре­сур­сов мень­ше тра­тить­ся... а по уму - на­до про­ве­рять, что из ри­су­емо­го по­па­да­ет в об­ласть об­нов­ле­ния, а что нет... А что­бы во­об­ще не мер­ца­ло - это на­до свои кон­т­ро­лы пи­сать и ста­вить там Do­ub­le­Buf­fe­red, ри­со­вать все са­мо­му, уби­рать pa­in­t­Bac­k­g­ro­und и пр.
    
    И пос­лед­нее - за­пуск тай­ме­ра:
    
    private vo­id pa­nel2_Mo­useC­lick(obj­ect sen­der, Mo­use­Even­tArgs e) {
    if (!ti­mer1.Enab­led) {
    timer1.Start();
    }
    }
    
    Можете про­ве­рять. Дол­ж­но выг­ля­деть при­мер­но так:
    
    
    
При­ло­же­ния.
    Примеры прог­рамм для ре­ше­ния ка­ких-то кон­к­рет­ных за­дач. Код с ком­мен­та­ри­ями. К каж­до­му при­ме­ру су­щес­т­ву­ет файл с ар­хи­вом про­ек­та для MS Vi­su­al Stu­dio 2005, ска­чать мож­но здесь: http://www.robinland.com/csharp-basis/samples.zip.
1. Самодельный MD5-хешер.
    
 
    Итак, для на­ча­ла соз­да­ем ок­но. Та­кое при­мер­но:
    
    Как это де­лать ду­маю по­яс­нять не на­до. До­бавь­те еще Open­Fi­le­Di­alog в не­го, в ко­то­ром за­дай­те в свой­ст­вах Fil­ter = "Все фай­лы (*.*)|*.*||".
    Теперь опи­сы­ва­ем все кноп­ки пос­ле­до­ва­тель­но (в ди­зай­не­ре дваж­ды щел­ка­ем на кноп­ку - соз­да­ет­ся фун­к­ция об­ра­бот­ки на­жа­тия):
    Открыть файл:
    
    
    private vo­id but­ton1_Click(obj­ect sen­der, Even­tArgs e) {
    if (open­Fi­le­Di­alog1.Show­Di­alog(this) == Di­alog­Re­sult.OK) { // от­к­ры­ва­ем ок­но "Открыть файл" и ес­ли в нем на­жа­ли ОК
    textBox1.Text = open­Fi­le­Di­alog1.Fi­le­Na­me; // пи­шем имя выб­ран­но­го фай­ла в пер­вом тек­с­то­вом по­ле
    CalculateHash(); // счи­та­ем хеш. Фун­к­цию на­пи­шем поз­же
    if (System.IO.Fi­le.Exis­ts(tex­t­Box1.Text + ".md5")) { // про­ве­ря­ем есть ли файл ‹имя_фай­ла_с_рас­ши­ре­ни­ем›.md5
    CompareHash(textBox1.Text + ".md5", fal­se); // про­ве­ря­ем хеш. Фун­к­цию на­пи­шем поз­же
    }
    else if (System.IO.Fi­le.Exis­ts(Path.Get­Di­rec­tor­y­Na­me(tex­t­Box1.Text) + "\" + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(tex­t­Box1.Text) + ".md5")) { // про­ве­ря­ем есть ли файл ‹имя_фай­ла›.md5
    CompareHash(Path.GetDirectoryName(textBox1.Text) + "\" + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(tex­t­Box1.Text) + ".md5", fal­se); // све­ря­ем хеш
    }
    }
    }
    Сохранить файл:
    
    
    private vo­id but­ton2_Click(obj­ect sen­der, Even­tArgs e) {
    if (System.IO.Fi­le.Exis­ts(tex­t­Box1.Text) tex­t­Box2.Text != "") { // ес­ли файл выб­ран и хеш для не­го пос­чи­тан
    StreamWriter sw = new Stre­am­W­ri­ter(Path.Get­Di­rec­tor­y­Na­me(tex­t­Box1.Text) + "\" + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(tex­t­Box1.Text) + ".md5", fal­se); // от­к­ры­ва­ем файл с пе­ре­за­писью с тем же име­нем, но с рас­ши­ре­ни­ем md5
    sw.WriteLine(textBox2.Text + " *" + Path.Get­Fi­le­Na­me(tex­t­Box1.Text)); // пи­шем в файл хеш и имя фай­ла
    sw.Close();
    }
    }
    Открыть файл с хе­шем:
    
    
    private vo­id but­ton3_Click(obj­ect sen­der, Even­tArgs e) {
    if (open­Fi­le­Di­alog1.Show­Di­alog(this) == Di­alog­Re­sult.OK) { // от­к­ры­ва­ем ди­алог "Открыть файл" и ес­ли в нем на­жа­ли ОК
    CompareHash(openFileDialog1.FileName, true); // све­ря­ем хеш
    }
    }
    Теперь пи­шем ос­нов­ные фун­к­ции:
    
    
    private vo­id Cal­cu­la­te­Hash() { // пос­чи­тать хеш
    if (System.IO.Fi­le.Exis­ts(tex­t­Box1.Text)) { // ес­ли файл су­щес­т­ву­ет
    FileStream fs = System.IO.Fi­le.Open(tex­t­Box1.Text, Fi­le­Mo­de.Open, Fi­le­Ac­cess.Re­ad, Fi­leS­ha­re.Re­ad); //откры­ва­ем файл
    byte[] hash = new byte[16]; // ре­зер­ви­ру­ем мес­то под хеш
    System.Security.Cryptography.MD5CryptoServiceProvider md5 = new System.Se­cu­rity.Cryptog­rap­hy.MD5Crypto­Ser­vi­ceP­ro­vi­der(); // соз­да­ем объ­ект уп­рав­ле­ния md5
    hash = md5.Com­pu­te­Hash(fs); // счи­та­ем хеш
    textBox2.Text = Has­h­ToS­t­ring(hash); // пи­шем хеш в тек­с­то­вое по­ле, пре­об­ра­зуя его че­рез на­шу фун­к­цию.
    }
    else {
    MessageBox.Show("Файл не су­щес­т­ву­ет или не выб­ран.", "Ошиб­ка"); // ес­ли файл не су­щес­т­ву­ет
    return;
    }
    }
    
    private vo­id Com­pa­re­Hash(string fi­le­na­me, bo­ol tryTo­Fin­d­Fi­le) { // срав­нить хе­ши из фай­ла fi­le­na­me и с ва­рин­том - ис­кать файл для ко­то­ро­го хеш был пос­чи­тан или нет
    StreamReader sr = new Stre­am­Re­ader(fi­le­na­me); // от­к­ры­ва­ем файл
    string md5str = sr.Re­ad­Li­ne(); // счи­ты­ва­ем стро­ку
    sr.Close(); // зак­ры­ва­ем файл
    if (md5str.Inde­xOf("*") == -1 || md5str.Inde­xOf(" ") == -1) { // ес­ли стро­ка не со­от­вес­т­ву­ет фор­ма­ту, т.е. не со­дер­жит про­бел или звез­доч­ку
    MessageBox.Show("Файл неп­ра­виль­но­го фор­ма­та.", "Ошиб­ка"); //по­ка­зать ошиб­ку
    return; // вер­нуть­ся, про­ве­рять не­че­го
    }
    string fi­le­ToC­heck = md5str.Sub­s­t­ring(md5str.Inde­xOf("*") + 1); // вы­ди­ра­ем из счи­тан­ной стро­ки имя фай­ла
    string has­h­ToC­heck = md5str.Sub­s­t­ring(0, md5str.Inde­xOf(" ")); // по­лу­ча­ем хеш из стро­ки
    textBox3.Text = has­h­ToC­heck.To­Up­per(); // пи­шем хеш в третье тек­с­то­вое по­ле
    fileToCheck = Path.Get­Di­rec­tor­y­Na­me(fi­le­na­me) + "\" + fi­le­ToC­heck; // кор­рек­ти­ру­ем имя фай­ла, пред­пи­сы­вая к не­му путь
    if (tryTo­Fin­d­Fi­le System.IO.Fi­le.Exis­ts(fi­le­ToC­heck)) { // ес­ли ис­кать файл с ко­то­ро­го хеш пос­чи­тан нуж­но и файл су­щес­т­ву­ет
    textBox1.Text = fi­le­ToC­heck; // пи­шем имя фай­ла в пер­вое тек­с­то­вое по­ле
    CalculateHash(); // счи­та­ем хеш
    }
    if (tex­t­Box2.Text == tex­t­Box3.Text) { // све­ря­ем на­пи­сан­ное во вто­ром и треть­ем тек­с­то­вых по­лях, ес­ли сов­па­ло
    textBox3.BackColor = System.Dra­wing.Co­lor.Lig­h­t­G­re­en; // под­с­ве­чи­ва­ем зе­ле­ным третье тек­с­то­вое по­ле
    }
    else { // ес­ли не сов­па­ло
    textBox3.BackColor = System.Dra­wing.Co­lor.Red; // под­с­ве­чи­ва­ем крас­ным
    }
    }
    private string Has­h­ToS­t­ring(byte[] hash) { // фун­к­ция пре­об­ра­зо­ва­ния хе­ша в стро­ку
    string ret = ""; // соз­да­ем пус­тую стро­ку
    for (int i = 0; i ‹ hash.Length; i++) { // для каж­до­го бай­та в мас­си­ве хе­ша
    ret += String.For­mat("{0:X1}", hash[i]); // пе­ре­вес­ти его в шес­т­над­ца­ти­рич­ную стро­ко­вую за­пись
    }
    return ret; // вер­нуть по­лу­чен­ную стро­ку
    }
    
    В об­щем-то прог­рам­ма го­то­ва, но она яв­но не до­тя­ги­ва­ет до удоб­ной. Хо­тя мож­но от­к­рыть лю­бой файл, тут же по­лу­чить MD5 хеш и тут же све­рить с кон­т­роль­ным фай­лом, ес­ли он ле­жит тут же и так же на­зы­ва­ет­ся. Мож­но от­к­рыть md5 файл и ав­то­ма­ти­чес­ки по­лу­чить срав­не­ние, ес­ли файл, ко­то­рый на­до про­ве­рить ле­жит тут же.
    Но все рав­но как-то бед­но... На­чи­на­ем ук­ра­ша­тель­с­т­ва.
    
    Включаем пе­ре­тас­ки­ва­ние (в ди­зай­не­ре вы­би­ра­ем tex­t­box1 по­том tex­t­box3, в окош­ке свойств дваж­ды щел­ка­ем на Dra­gEn­ter и Drag­D­rop):
    
    
    private vo­id tex­t­Box1_Dra­gEn­ter(obj­ect sen­der, Dra­gE­ven­tArgs e) { // ког­да кур­сор над тек­с­то­вым по­лем
    if (e.Da­ta.Get­Da­taP­re­sent("Fi­le­Na­meW")) { // ес­ли та­щи­мый объ­ект со­дер­жит ин­фор­ма­цию об име­ни фай­ла
    e.Effect = Drag­D­ro­pEf­fec­ts.Link; // сме­нить кур­сор на "Соз­дать яр­лык"
    }
    }
    
    private vo­id tex­t­Box1_Drag­D­rop(obj­ect sen­der, Dra­gE­ven­tArgs e) { // ког­да что-то бро­си­ли на по­ле
    textBox1.Text = ((string[])e.Da­ta.Get­Da­ta("Fi­le­Na­meW"))[0]; // по­лу­чить имя фай­ла и за­пи­сать в пер­вое тек­с­то­вое по­ле
    CalculateHash(); // пос­чи­тать хеш
    if (System.IO.Fi­le.Exis­ts(tex­t­Box1.Text + ".md5")) { // про­ве­рить на на­ли­чие фай­ла md5
    CompareHash(textBox1.Text + ".md5", fal­se);
    }
    else if (System.IO.Fi­le.Exis­ts(Path.Get­Di­rec­tor­y­Na­me(tex­t­Box1.Text) + "\" + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(tex­t­Box1.Text) + ".md5")) {
    CompareHash(Path.GetDirectoryName(textBox1.Text) + "\" + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(tex­t­Box1.Text) + ".md5", fal­se);
    }
    }
    
    private vo­id tex­t­Box3_Dra­gEn­ter(obj­ect sen­der, Dra­gE­ven­tArgs e) { // ког­да кур­сор над тек­с­то­вым по­лем
    if (e.Da­ta.Get­Da­taP­re­sent("Fi­le­Na­meW")) { // ес­ли та­щи­мый объ­ект со­дер­жит ин­фор­ма­цию об име­ни фай­ла
    e.Effect = Drag­D­ro­pEf­fec­ts.Link; // сме­нить кур­сор на "Соз­дать яр­лык"
    }
    }
    
    private vo­id tex­t­Box3_Drag­D­rop(obj­ect sen­der, Dra­gE­ven­tArgs e) { // ког­да что-то бро­си­ли на по­ле
    CompareHash(((string[])e.Data.GetData("FileNameW"))[0], true); // срав­нить хеш из фай­ла уро­не­но­го объ­ек­та и про­ве­рить на ори­ги­наль­ный файл
    }
    
    Теперь вклю­ча­ем кноп­ки вно­са в ре­естр и уда­ле­ния из ре­ес­т­ра:
    Тут нас под­жи­да­ет проб­ле­ма - пункт кон­тек­с­т­но­го ме­ню Пос­лать (Send To) - это пап­ка с яр­лы­ка­ми, а не ключ ре­ес­т­ра. А яр­лы­ки соз­да­ют­ся слож­но - толь­ко с по­мощью до­пол­ни­тель­ной биб­ли­оте­ки, на ко­то­рую на­до до­ба­вить ссыл­ку (Re­fe­ren­ce). Нап­ри­мер, на файл C:\Win­dows\system32\wshom.ocx.
    
    
    private vo­id but­ton4_Click(obj­ect sen­der, Even­tArgs e) { // внес­ти в ре­естр
    RegistryKey rk = Re­gis­t­ry.Clas­ses­Ro­ot; // от­к­ры­ва­ем ветвь ре­ес­т­ра HKEY_CLAS­SES_RO­OT
    rk = rk.Cre­ate­Sub­Key(".md5\\shell\\Open\\com­mand"); // соз­да­ем (или от­к­ры­ва­ем, ес­ли уже есть) ветвь .md5\\shell\\Open\\com­mand
    rk.SetValue("", """ + Ap­pli­ca­ti­on.Exe­cu­tab­le­Path + "" %1"); // пи­шем в ключ (По умол­ча­нию) путь к за­пу­щен­ной прог­рам­ме и ука­за­тель ар­гу­мен­та
    rk.Close(); // зак­ры­ва­ем ключ
    IWshShell ws = new IW­s­h­Run­ti­me­Lib­rary.WshShell(); // соз­да­ем объ­ект биб­ли­оте­ки Shell
    IWshShortcut sc = (IWshShor­t­cut)ws.Cre­ateS­hor­t­cut(System.Envi­ron­ment.Get­Fol­der­Path(System.Envi­ron­ment.Spe­ci­al­Fol­der.Sen­d­To) + "\" + "md5has­her.lnk"); //соз­да­ем файл md5has­her.lnk в пап­ке Sen­d­To те­ку­ще­го поль­зо­ва­те­ля
    sc.TargetPath = Ap­pli­ca­ti­on.Exe­cu­tab­le­Path; // про­пи­сы­ва­ем путь к за­пус­к­но­му фай­лу
    sc.WindowStyle = 1; // тип ок­на - нор­маль­ный
    sc.WorkingDirectory = Path.Get­Di­rec­tor­y­Na­me(Appli­ca­ti­on.Exe­cu­tab­le­Path); // ра­бо­чая ди­рек­то­рия
    sc.IconLocation = Ap­pli­ca­ti­on.Exe­cu­tab­le­Path + ", 0"; // икон­ка
    sc.Save(); // сох­ра­ня­ем файл
    }
    
    private vo­id but­ton5_Click(obj­ect sen­der, Even­tArgs e) { // уб­рать из ре­ес­т­ра
    RegistryKey rk = Re­gis­t­ry.Clas­ses­Ro­ot; // от­к­ры­ва­ем ветвь ре­ес­т­ра HKEY_CLAS­SES_RO­OT
    rk.DeleteSubKeyTree(".md5"); // уда­ля­ем ключ и все под­чи­нен­ные
    rk.Close(); // зак­ры­ва­ем ключ
    if (System.IO.Fi­le.Exis­ts(System.Envi­ron­ment.Get­Fol­der­Path(System.Envi­ron­ment.Spe­ci­al­Fol­der.Sen­d­To) + "\" + "md5has­her.lnk")) { // ес­ли файл ник­то не уда­лил
    System.IO.File.Delete(System.Environment.GetFolderPath(System.Environment.SpecialFolder.SendTo) + "\" + "md5has­her.lnk"); // уда­ля­ем его
    }
    }
    И для об­ра­бот­ки ар­гу­мен­та в ком­ман­д­ной стро­ке до­бав­лем та­кую фун­к­цию (дваж­ды щел­ка­ем в ди­зай­не­ре на пус­том мес­те фор­мы):
    
    private vo­id Form1_Lo­ad(obj­ect sen­der, Even­tArgs e) { // при заг­руз­ке ок­на
    if (Envi­ron­ment.Get­Com­man­d­Li­ne­Ar­gs().Length › 1) { // ес­ли есть ар­гу­мен­ты ко­ман­д­ной стро­ки
    if (Path.Ge­tEx­ten­si­on(Envi­ron­ment.Get­Com­man­d­Li­ne­Args()[1]) == ".md5") { // ес­ли пер­вый ар­гу­мент окан­чи­ва­ет­ся на md5
    CompareHash(Environment.GetCommandLineArgs()[1], true); // срав­нить хеш, с по­ис­ком ори­ги­наль­но­го фай­ла
    }
    else { // ес­ли нет
    textBox1.Text = En­vi­ron­ment.Get­Com­man­d­Li­ne­Args()[1]; // внес­ти пер­вый ар­гу­мент в пер­вое тек­с­то­вое по­ле
    CalculateHash(); // пос­чи­тать хеш
    }
    }
    }
    
    Не за­бы­вай­те встав­лять но­вые ссыл­ки на na­mes­pa­ce. Под ко­нец ра­бо­ты спи­сок using выг­ля­дел так:
   
    using System;
    using System.Com­po­nen­t­Mo­del;
    using System.Win­dows.Forms;
    using System.IO;
    using Mic­ro­soft.Win32;
    using IW­s­h­Run­ti­me­Lib­rary;
    
    Программа не про­ве­ря­ет за­пу­ще­на ли она уже, так что всег­да за­пус­ка­ет­ся но­вая. Да и пе­ре­тас­ки­ва­ние ра­бо­та­ет толь­ко над тек­с­то­вы­ми ок­на­ми. Но вы ведь, как ав­тор, об этом зна­ете и не оши­бе­тесь?
    
    Оставшийся не­дос­та­ток - прог­рам­ма ра­бо­та­ет толь­ко с од­ним фай­лом. Т.е. нель­зя по­лу­чить md5 хеш нес­коль­ких фай­лов или це­ло­го ка­та­ло­га сра­зу. На­до счи­тать их от­дель­но.
    Каталог про­ек­та для MS Vi­su­al Stu­dio 2005 с при­ме­ром на­хо­дит­ся в ар­хи­ве при­ме­ров, под­ка­та­лог md5has­her.
    
2. Самодельная головоломка.
    
    попробуем сде­лать прос­тую го­ло­во­лом­ку. А что­бы она не бы­ла ин­те­рес­на толь­ко нам - поп­ро­бу­ем ее ук­ра­сить.
    Итак, идея го­ло­во­лом­ки - по­ле 6х6. На нем рас­по­ло­же­ны фиш­ки 6 цве­тов и 6 форм - ито­го 36 фи­шек. Рас­по­ло­же­ны слу­чай­но, в на­ча­ле иг­ры. За­да­ча - рас­по­ло­жить их так, что­бы в ря­дах рас­по­ла­га­лись фиш­ки оди­на­ко­во­го цве­та и фор­мы. Ли­бо по го­ри­зон­та­ли один цвет, по вер­ти­ка­ли - од­на фор­ма, ли­бо на­обо­рот. Вро­де это­го:
    
 
    Фишки мож­но ме­нять мес­та­ми с со­сед­ни­ми по вер­ти­ка­ли, го­ри­зон­та­ли и ди­аго­на­ли, ес­ли со­сед­няя то­го же цве­та или той же фор­мы. Вро­де все прос­то. Сво­е­об­раз­ные пят­наш­ки по­лу­ча­ют­ся.
    Приступим. Соз­да­ем ок­но. Нам по­на­до­бят­ся кноп­ки Но­вой иг­ры, Нас­т­рой­ки и Вы­хо­да. Ос­таль­ное прос­т­ран­с­т­во под иг­ро­вое по­ле. Пос­коль­ку ри­сов­ка прос­тая - ис­поль­зу­ем дви­жок GDI+, без под­к­лю­че­ния OpenGL или Di­rectX. Ри­со­вать бу­дем на эле­мен­те pa­nel.
    
    Сначала за­да­ем нуж­ные нам по­ля...
    
    #region fi­elds
    internal Ga­me­Op­ti­ons op­ti­ons; //нас­т­рой­ки иг­ры, класс соз­да­дим поз­д­нее
    internal Fis­h­ka[] fis­h­ki; // соб­с­т­вен­но фиш­ки, класс соз­да­дим поз­д­нее
    internal bo­ol Is­Ga­me; // флаг что иг­ра идет
    internal So­lid­B­rush[] brus­hes; // кис­ти для зак­рас­ки фи­шек
    internal bo­ol eog; // флаг кон­ца иг­ры
    private Grap­hics pa­nelGr; // объ­ект, че­рез ко­то­рый бу­дем ри­со­вать
    #endregion
    
    У эле­мен­та Pa­nel есть свой объ­ект клас­са Grap­hics, но его уж очень не­удоб­но ис­поль­зо­вать. Нам по­на­до­бит­ся дос­туп к гра­фи­ке не толь­ко из со­бы­тия Pa­int, но и из дру­гих фун­к­ций­, так что про­ще соз­дать свой Grap­hics объ­ект и при­пи­сать его к Pa­nel. Это не луч­ший путь, но в на­шем слу­чае он под­хо­дит луч­шим об­ра­зом, на мой взгляд.
    Дальше про­во­дим ини­ци­али­за­цию соз­дан­ных по­лей и вклю­ча­ем кноп­ки.
    
    
    private vo­id Form1_Lo­ad(obj­ect sen­der, Even­tArgs e) {
    options = new Ga­me­Op­ti­ons(); // соз­да­ем объ­ект нас­т­ро­ек с зна­че­ни­ями по умол­ча­нию
    fishki = new Fis­h­ka[36]; // соз­да­ем фиш­ки
    brushes = new So­lid­B­rush[6]; // соз­да­ем прос­тые кис­ти
    InitBrushes(); // ини­ци­али­зи­ру­ем их
    //инициализируем фиш­ки
    for (byte i = 0; i ‹ 6; i++) {
    for (byte j = 0; j ‹ 6; j++) {
    fishki[i * 6 + j] = new Fis­h­ka(j, i, i, j); // соз­да­ем фиш­ки с па­ра­мет­ра­ми.
    }
    }
    panelGr = Grap­hics.From­H­w­nd(pa­nel1.Han­d­le); // соз­да­ем объ­ект ри­со­ва­ния для pa­nel1
    NewGame(); // Ини­ци­али­зи­ру­ем иг­ру
    }
    
    #region but­tons
    private vo­id but­ton1_Click(obj­ect sen­der, Even­tArgs e) { // кноп­ка Но­вая иг­ра
    NewGame();
    }
    private vo­id but­ton3_Click(obj­ect sen­der, Even­tArgs e) { // Кноп­ка Нас­т­рой­ка
    SetOptions();
    }
    private vo­id but­ton2_Click(obj­ect sen­der, Even­tArgs e) { // Кноп­ка Вы­ход
    Close();
    }
    #endregion
    
    Теперь соз­да­ем все то, что мы уже упо­мя­ну­ли:
    
    
    internal class Ga­me­Op­ti­ons // класс нас­т­ро­ек иг­ры
    {
    internal Co­lor[] fis­h­ka­Co­lors; // мас­сив выб­ран­ных цве­тов для фи­шек
    internal Ga­me­Op­ti­ons() { // кон­с­т­рук­тор с зна­че­ни­ями по умол­ча­нию
    fishkaColors = new Co­lor[6];
    fishkaColors[0] = Co­lor.Red;
    fishkaColors[1] = Co­lor.Lig­h­t­G­re­en;
    fishkaColors[2] = Co­lor.Blue;
    fishkaColors[3] = Co­lor.Cyan;
    fishkaColors[4] = Co­lor.Ma­gen­ta;
    fishkaColors[5] = Co­lor.Oran­ge;
    }
    }
    
    internal class Fis­h­ka // класс фи­шек
    {
    internal byte sha­pe; // но­мер фор­мы фиш­ки
    internal byte co­lor; // но­мер цве­та фиш­ки
    internal byte col; // стол­бец
    internal byte row; // ряд
    internal Fis­h­ka() { // кон­с­т­рук­тор с зна­че­ни­ями по умол­ча­нию
    shape = 0;
    color = 0;
    col = 0;
    row = 0;
    }
    internal Fis­h­ka(byte in­S­ha­pe, byte in­Co­lor, byte in­Col, byte in­Row) { // кон­с­т­рук­тор
    shape = in­S­ha­pe;
    color = in­Co­lor;
    col = in­Col;
    row = in­Row;
    }
    }
    
    Теперь фун­к­ции:
    
    internal vo­id Init­B­rus­hes() { // ини­ци­али­за­ция кис­тей
    for (int i = 0; i ‹ 6; i++) {
    brushes[i] = new So­lid­B­rush(opti­ons.fis­h­ka­Co­lors[i]); // соз­да­ем кисть с за­дан­ным цве­том
    }
    GC.Collect(); // уби­ра­ем му­сор из па­мя­ти.
    }
    
    internal vo­id New­Ga­me() { // ини­ци­али­за­ция иг­ры
    //ставим фиш­ки на слу­чай­ные мес­та
    Random rnd = new Ran­dom();
    byte tmpCol, tmpRow;
    int tmpInt;
    for (int i = 0; i ‹ 36; i++) { // про­во­дим 36 слу­чай­ных за­мен мест
    tmpCol = fis­h­ki[i].col;
    tmpRow = fis­h­ki[i].row;
    tmpInt = rnd.Next(0, 35);
    fishki[i].col = fis­h­ki[tmpInt].col;
    fishki[i].row = fis­h­ki[tmpInt].row;
    fishki[tmpInt].col = tmpCol;
    fishki[tmpInt].row = tmpRow;
    }
    eog = fal­se; // сни­ма­ем флаг окон­ча­ния иг­ры
    IsGame = true; // иг­ра на­ча­лась
    panel1.Refresh(); // пе­ре­ри­со­вы­ва­ем по­ле
    }
    
    internal vo­id Se­tOp­ti­ons() { //за­да­ние нас­т­ро­ек
    IsGame = fal­se; // иг­ру на па­узу
    Form2 set­tin­g­s­Form = new Form2(this); // от­к­ры­ва­ем ок­но нас­т­ро­ек
    settingsForm.StartPosition = For­m­S­tar­t­Po­si­ti­on.Cen­ter­Pa­rent; // от­к­ры­вать нуж­но по цен­т­ру ос­нов­но­го ок­на
    if (set­tin­g­s­Form.Show­Di­alog(this) == Di­alog­Re­sult.OK) { // от­к­ры­ва­ем ди­алог нас­т­ро­ек и ес­ли на­жа­ли ОК
    InitBrushes(); // пе­ре­ини­ци­али­зи­ру­ем кис­ти
    }
    IsGame = true; // сни­ма­ем иг­ру с па­узы
    panel1.Refresh(); // пе­ре­ри­со­вы­ва­ем по­ле
    }
    private vo­id pa­nel1_Pa­int(obj­ect sen­der, Pa­in­tE­ven­tArgs e) { // фун­к­ция ри­со­ва­ния по­ля
    if (!IsGa­me) { re­turn; } // ес­ли нет иг­ры - не ри­со­вать
    for (byte i = 0; i ‹ 36; i++) {
    DrawFishka(i); // ри­су­ем фиш­ки
    }
    }
    
    Теперь соб­с­т­вен­но ри­су­ем фиш­ки:
    
    internal vo­id Draw­Fis­h­ka(byte i) {
    int fWidth = 42; // ши­ри­на фиш­ки
    int fHe­ight = 42; // вы­со­та фиш­ки
    int x = 4 + fis­h­ki[i].col * 50; // на­ча­ло фиш­ки по Х
    int y = 4 + fis­h­ki[i].row * 50; // на­ча­ло виш­ки по Y
    panelGr.FillRectangle(SystemBrushes.Control, x - 1, y - 1, fWidth + 2, fHe­ight + 2); // зак­ра­ши­ва­ем по­ле фиш­ки цве­том фо­на
    switch (fis­h­ki[i].sha­pe) { // ри­су­ем фиш­ки раз­ной фор­мы
    case 0:
    panelGr.FillRectangle(brushes[fishki[i].color], x, y, fWidth, fHe­ight); // квад­рат
    break;
    case 1:
    panelGr.FillEllipse(brushes[fishki[i].color], x, y, fWidth, fHe­ight); // круг
    break;
    case 2:
    panelGr.FillPie(brushes[fishki[i].color], x - fWidth/2, y, fWidth*2, fHe­ight*2, -120, 60); // сек­тор
    break;
    case 3:
    panelGr.FillPolygon(brushes[fishki[i].color], new Po­int[] { new Po­int(x, y + fHe­ight), new Po­int(x + fWidth, y + fHe­ight), new Po­int(x + fWidth/2, y) }); // тре­уголь­ник вверх
    break;
    case 4:
    panelGr.FillPolygon(brushes[fishki[i].color], new Po­int[] { new Po­int(x + fWidth, y + fHe­ight), new Po­int(x + fWidth, y), new Po­int(x, y + fHe­ight/2) }); // тре­уголь­ник вле­во
    break;
    case 5:
    panelGr.FillPolygon(brushes[fishki[i].color], new Po­int[] { new Po­int(x, y + fHe­ight), new Po­int(x, y), new Po­int(x + fWidth, y + fHe­ight/2) }); // тре­уголь­ник впра­во
    break;
    }
    }
    На этом мо­мен­те по­ле ри­су­ет­ся, но не уп­рав­ля­ет­ся.
    
    Приступаем к соз­да­нию уп­рав­ле­ния. Нам на­до, что­бы мож­но бы­ло мыш­кой вы­би­рать фиш­ку - зна­чит нуж­но ку­да за­пи­сы­вать выб­ран­ную. До­пи­сы­ва­ем в ре­ги­он fi­elds та­кую за­пись:
    
    internal byte cur­Se­lec­ted; // ин­декс выб­ран­ной фиш­ки
    
    А в фун­к­цию ини­ци­али­за­ции (Form1_Lo­ad) та­кую:
    
    curSelected = 255; // ни­ка­кая не выб­ра­на
    
    Теперь опи­сы­ва­ем фун­к­цию кли­ка мыш­ки, но по­коль­ку нам нуж­ны точ­ные ко­ор­ди­на­ты кли­ка, возь­мем со­бы­тие Mo­use­Up:
    
    private vo­id pa­nel1_Mo­use­Up(obj­ect sen­der, Mo­use­Even­tArgs e) {
    if (eog || !IsGa­me) { re­turn; } // ес­ли иг­ра окон­че­на или на па­узе - не об­ра­ба­ты­вать
    byte f = Get­Fis­h­kaF­rom­Co­ord(e.X, e.Y); // по­лу­чить ин­декс фиш­ки по ко­ор­ди­на­там кли­ка
    if (f != cur­Se­lec­ted) { // ес­ли клик­ну­ли не на уже выб­ран­ную
    if (f == 255) { // ес­ли клик­ну­ли вне по­ля
    curSelected = f; // ни­ка­кая не выб­ра­на
    return;
    }
    if (cur­Se­lec­ted == 255) { cur­Se­lec­ted = f; } // ес­ли ник­то не был выб­ран, го­во­рим что бы­ла выб­ра­на она же
    else if (Chec­kAn­d­S­wit­ch(cur­Se­lec­ted, f)) { re­turn; } // про­ве­ря­ем на­до ли фиш­ки ме­нять мес­та­ми, ес­ли да - воз­в­ра­ща­ем­ся
    curSelected = f; // выб­ра­на но­вая
    }
    else if (f != 255) { // ес­ли клик­ну­ли на ту же фиш­ку
    curSelected = 255; // ни­ка­кая не выб­ра­на
    }
    }
    internal byte Get­Fis­h­kaF­rom­Co­ord(int x, int y) { // по­лу­чить ин­декс фиш­ки по ко­ор­ди­на­там
    byte col = (byte)(x / 50); // по­лу­чить стол­бец
    byte row = (byte)(y / 50); // по­лу­чить ряд
    for (byte i = 0; i ‹ 36; i++) { //пос­ле­до­ва­тель­но про­ве­рить, кто здесь сто­ит
    if (fis­h­ki[i].col == col fis­h­ki[i].row == row) {
    return i;
    }
    }
    return 255; // ес­ли ник­то - вер­нуть ник­то
    }
    Последовательные пе­ре­бор - пло­хое ре­ше­ние, но ес­ли в мас­си­ве 36 объ­ек­тов, то на сов­ре­мен­ных ком­пах сой­дет. Луч­ше ко­неч­но соз­дать пар­ный мас­сив мест, с пос­то­ян­но под­дер­жи­ва­емой ин­дек­са­ци­ей ка­кая фиш­ка сто­ит в i мес­те.
    Теперь фун­к­ция сме­ны фи­шек:
    
    internal bo­ol Chec­kAn­d­S­wit­ch(byte f1, byte f2) {
    //проверяем со­сед­ние ли фиш­ки
    if (Math.Abs(fis­h­ki[f1].col - fis­h­ki[f2].col) ‹= 1 Math.Abs(fis­h­ki[f1].row - fis­h­ki[f2].row) ‹= 1) {
    //проверяем сов­па­да­ет цвет или фор­ма
    if (fis­h­ki[f1].co­lor == fis­h­ki[f2].co­lor || fis­h­ki[f1].sha­pe == fis­h­ki[f2].sha­pe) {
    //меняем мес­та­ми
    byte tc = fis­h­ki[f1].col;
    byte tr = fis­h­ki[f1].row;
    fishki[f1].col = fis­h­ki[f2].col;
    fishki[f1].row = fis­h­ki[f2].row;
    fishki[f2].col = tc;
    fishki[f2].row = tr;
    //снимаем выб­ран­ность
    curSelected = 255;
    //перерисовываем
    DrawFishka(f1);
    DrawFishka(f2);
    //проверяем не окон­че­на ли иг­ра
    eog = true;
    // смот­рим на фор­мы по го­ри­зон­та­ли и цве­та по вер­ти­ка­ли
    for (int i = 0; i ‹ 6; i++) {
    for (int j = 0; (j ‹ 5 eog); j++) {
    if (j != 5 fis­h­ki[i * 6 + j].row != fis­h­ki[i * 6 + j + 1].row) {
    eog = fal­se;
    }
    if (i != 5 fis­h­ki[i * 6 + j].col != fis­h­ki[(i + 1) * 6 + j].col) {
    eog = fal­se;
    }
    }
    }
    if (eog) { // ес­ли оши­бок нет
    DrawEOG(); // ри­су­ем ко­нец иг­ры
    }
    else { // ес­ли ошиб­ки бы­ли - про­ве­ря­ем на­обо­рот
    eog = true;
    for (int i = 0; i ‹ 6; i++) {
    for (int j = 0; (j ‹ 5 eog); j++) {
    if (j != 5 fis­h­ki[i * 6 + j].col != fis­h­ki[i * 6 + j + 1].col) {
    eog = fal­se;
    }
    if (i != 5 fis­h­ki[i * 6 + j].row != fis­h­ki[(i + 1) * 6 + j].row) {
    eog = fal­se;
    }
    }
    }
    if (eog) { // оши­бок нет
    DrawEOG(); // ко­нец иг­ры
    }
    }
    return true; // фиш­ки ме­ня­ли мес­та­ми
    }
    }
    return fal­se; // фиш­ки не ме­ня­ли мес­та­ми
    }
    
    Осталось пос­лед­нее - на­ри­со­вать ко­нец иг­ры:
    
    internal vo­id Dra­wE­OG() {
    panelGr.DrawString("Собрано", new Font("Ti­mes New Ro­man", 80, Grap­hic­sU­nit.Pi­xel), Brus­hes.Black, 0, 100); // ри­су­ем сло­во Соб­ра­но по­верх по­ля
    }
    
    В об­щем и це­лом - иг­ра ра­бо­та­ет. Дру­гое де­ло, что она не кра­си­вая и не удоб­ная. Нас­т­рой­ку цве­тов мы пре­дус­мот­ре­ли, но по­ка не сде­ла­ли. Да и иг­рать, ког­да не вид­но ка­кая фиш­ка выб­ра­на - тя­же­ло. Поп­ро­бу­ем нем­но­го ук­ра­сить иг­ру.
    Во-первых вве­дем ин­ди­ка­цию над ка­кой фиш­кой на­хо­дит­ся мыш­ка и бу­дем от­ри­со­вы­вать выб­ран­ную фиш­ку, и ту над ко­то­рой мыш­ка.
    Добавим ин­декс "под-мы­шеч­ной­" фиш­ки:
    
    #region fi­elds
    internal byte cur­Ho­ve­red;
    //Form1_Load
    curHovered = 255;
    
    Для кон­т­ро­ля опи­шем фун­к­цию дви­же­ния мыш­ки по по­лю:
    
    private vo­id pa­nel1_Mo­use­Mo­ve(obj­ect sen­der, Mo­use­Even­tArgs e) {
    if (eog || !IsGa­me) { re­turn; } // ес­ли иг­ра окон­че­на или на па­узе - не ре­аги­ро­вать
    byte f = Get­Fis­h­kaF­rom­Co­ord(e.X, e.Y); // по­лу­чить фиш­ку по ко­ор­ди­на­там
    if (f == 255 cur­Ho­ve­red != 255) { // ес­ли мыш­ка вне по­ля, но ка­кая-то фиш­ка бы­ла от­ме­че­на - сни­ма­ем от­мет­ку
    curHovered = 255;
    return;
    }
    if (f != cur­Ho­ve­red) { // ес­ли мыш­ка пе­реш­ла на дру­гую фиш­ку, ме­ня­ем от­мет­ку
    curHovered = f;
    }
    }
    
    Теперь на­до от­ри­со­вы­вать выб­ран­ность фи­шек и их ре­ак­цию на мыш­ку. А за­од­но, хо­ро­шо бы и ри­со­вать их по­луч­ше.
    Изменим фун­к­ции ре­ак­ций на мыш­ку так, что­бы фиш­ки пе­ре­ри­со­вы­ва­лись:
    
    #region mo­use events
    private vo­id pa­nel1_Mo­use­Mo­ve(obj­ect sen­der, Mo­use­Even­tArgs e) { // дви­же­ние мыш­ки по по­лю
    if (eog || !IsGa­me) { re­turn; }
    byte f = Get­Fis­h­kaF­rom­Co­ord(e.X, e.Y);
    if (f == 255 cur­Ho­ve­red != 255) {
    f = cur­Ho­ve­red; // за­по­ми­на­ем ка­кая бы­ла от­ме­че­на
    curHovered = 255; // сни­ма­ем от­мет­ку
    DrawFishka(f); // пе­ре­ри­со­вы­ва­ем фиш­ку, ко­то­рая бы­ла от­ме­че­на, что­бы снять от­мет­ку
    return;
    }
    if (f != cur­Ho­ve­red) {
    if (cur­Ho­ve­red == 255) { cur­Ho­ve­red = f; }
    byte old = cur­Ho­ve­red; // за­по­ми­на­ем ка­кая бы­ла от­ме­ча­на
    curHovered = f; // ме­ня­ем от­мет­ку
    DrawFishka(old); // пе­ре­ри­со­вы­ва­ем струю
    DrawFishka(curHovered); // и но­вую
    }
    }
    
    private vo­id pa­nel1_Mo­use­Up(obj­ect sen­der, Mo­use­Even­tArgs e) { // клик мыш­ки
    if (eog || !IsGa­me) { re­turn; }
    byte f = Get­Fis­h­kaF­rom­Co­ord(e.X, e.Y);
    if (f != cur­Se­lec­ted) {
    if (f == 255) {
    f = cur­Se­lec­ted; // за­по­ми­на­ем, ка­кая бы­ла выб­ра­на
    curSelected = 255; // сни­ма­ем выб­ран­ность
    DrawFishka(f); // пе­ре­ри­со­вы­ва­ем ста­рую, что­бы снять вы­де­ле­ние
    return;
    }
    if (cur­Se­lec­ted == 255) { cur­Se­lec­ted = f; }
    else if (Chec­kAn­d­S­wit­ch(cur­Se­lec­ted, f)) { re­turn; }
    byte old = cur­Se­lec­ted; // за­по­ми­на­ем, ка­кая бы­ла выб­ра­на
    curSelected = f; // ме­ня­ем от­мет­ку
    DrawFishka(old); // пе­ре­ри­со­вы­ва­ем ста­рую
    DrawFishka(curSelected); // и но­вую
    }
    else if (f != 255) {
    f = cur­Se­lec­ted; // за­по­ми­на­ем ка­кая бы­ла выб­ра­на
    curSelected = 255; // сни­ма­ем вы­де­ле­ние
    DrawFishka(f); // пе­ре­ри­со­вы­ва­ем ста­рую
    }
    }
    #endregion
    
    Ну и на­ко­нец ри­со­ва­ние... до­ба­вим гра­ди­ен­т­ную за­лив­ку к фиш­кам. А что­бы бы­ло про­ще ме­нять спо­со­бы за­лив­ки - вве­дем соз­да­ние кис­тей в фун­к­цию ри­со­ва­ния. Не­ра­ци­ональ­но, но при столь ма­лом ри­со­ва­нии - поч­ти не за­мет­но. Итак фун­к­ция Init­B­rush, все ее упо­ми­на­ния и по­ле brus­hes не нуж­ны. А фун­к­цию ри­со­ва­ния сде­ла­ем та­кой­:
    
    internal vo­id Draw­Fis­h­ka(byte i) {
    int fWidth = 42;
    int fHe­ight = 42;
    int x = 4 + fis­h­ki[i].col * 50;
    int y = 4 + fis­h­ki[i].row * 50;
    panelGr.FillRectangle(SystemBrushes.Control, x - 1, y - 1, fWidth + 2, fHe­ight + 2);
    LinearGradientBrush br; // дек­ла­ри­ру­ем кисть с гра­ди­ен­том
    if (cur­Se­lec­ted == i) { // ес­ли ри­су­емая фиш­ка - выб­ра­на
    br = new Li­ne­ar­G­ra­di­en­t­B­rush(new Po­int(x,y), new Po­int(x+fWidth, y+fHe­ight), Co­lor.Gray, op­ti­ons.fis­h­ka­Co­lors[fis­h­ki[i].co­lor]); // соз­да­ем гра­ди­ент от се­ро­го к цве­ту фиш­ки
    }
    else if (cur­Ho­ve­red == i) { // ес­ли ри­су­емая фиш­ка под мыш­кой
    br = new Li­ne­ar­G­ra­di­en­t­B­rush(new Po­int(x, y), new Po­int(x + fWidth, y + fHe­ight), op­ti­ons.fis­h­ka­Co­lors[fis­h­ki[i].co­lor], Co­lor.Gray); // соз­да­ем гра­ди­ент от цве­та фиш­ки к се­ро­му
    }
    else { // ес­ли это прос­то фиш­ка
    br = new Li­ne­ar­G­ra­di­en­t­B­rush(new Po­int(x, y), new Po­int(x + fWidth, y + fHe­ight), op­ti­ons.fis­h­ka­Co­lors[fis­h­ki[i].co­lor], Co­lor.Whi­te); // гра­ди­ент от цве­та фиш­ки к бе­ло­му
    }
    switch (fis­h­ki[i].sha­pe) {
    case 0:
    panelGr.FillRectangle(br, x, y, fWidth, fHe­ight);
    break;
    case 1:
    panelGr.FillEllipse(br, x, y, fWidth, fHe­ight);
    break;
    case 2:
    panelGr.FillPie(br, x - fWidth/2, y, fWidth*2, fHe­ight*2, -120, 60);
    break;
    case 3:
    panelGr.FillPolygon(br, new Po­int[] { new Po­int(x, y + fHe­ight), new Po­int(x + fWidth, y + fHe­ight), new Po­int(x + fWidth/2, y) });
    break;
    case 4:
    panelGr.FillPolygon(br, new Po­int[] { new Po­int(x + fWidth, y + fHe­ight), new Po­int(x + fWidth, y), new Po­int(x, y + fHe­ight/2) });
    break;
    case 5:
    panelGr.FillPolygon(br, new Po­int[] { new Po­int(x, y + fHe­ight), new Po­int(x, y), new Po­int(x + fWidth, y + fHe­ight/2) });
    break;
    }
    }
    
    Осталось сде­лать вы­бор цве­тов.
 
 
    Делаем та­кую фор­му:
    
    Не за­будь­те до­ба­вить Co­lor­Di­alog, ука­зать Ac­cep­t­But­ton = but­ton7 для фор­мы.
    Инициализируем фор­му:
    
    private Form1 pf;
    
    internal Form2(Form1 inFrm) { // кон­с­т­рук­тор с ука­за­ни­ем ро­ди­тель­с­кой фор­мы
    InitializeComponent();
    pf = inFrm; // за­да­ем ука­за­тель на ро­ди­тель­с­кую фор­му
    button1.BackColor = pf.opti­ons.fis­h­ka­Co­lors[0]; // за­да­ем цве­та для кно­пок
    button2.BackColor = pf.opti­ons.fis­h­ka­Co­lors[1];
    button3.BackColor = pf.opti­ons.fis­h­ka­Co­lors[2];
    button4.BackColor = pf.opti­ons.fis­h­ka­Co­lors[3];
    button5.BackColor = pf.opti­ons.fis­h­ka­Co­lors[4];
    button6.BackColor = pf.opti­ons.fis­h­ka­Co­lors[5];
    }
    
    Дальше есть два пу­ти. Пер­вый обыч­ный­: для каж­дой кноп­ки пи­шем та­кую фун­к­цию:
    
    private vo­id but­ton1_Click(obj­ect sen­der, Even­tArgs e) {
    colorDialog1.Color = but­ton1.Bac­k­Co­lor; // ус­та­нав­ли­ва­ем цвет фиш­ки в ди­ало­ге вы­бо­ра цве­та
    if (co­lor­Di­alog1.Show­Di­alog(this) == Di­alog­Re­sult.OK) { // от­к­ры­ва­ем ди­алог, и ес­ли в нем на­жа­ли ОК
    button1.BackColor = co­lor­Di­alog1.Co­lor; // ус­та­нав­ли­ва­ем выб­ран­ный цвет на кноп­ку
    pf.options.fishkaColors[0] = co­lor­Di­alog1.Co­lor; // и на фиш­ку
    }
    }
    
    И так 6 раз. За­нуд­но, но прос­то. Вто­рой путь - за­дать для каж­дой кноп­ки па­ра­метр Tag = но­ме­ру цве­та. Т.е. 0, 1, 2, 3, 4, 5 в по­ряд­ке соз­да­ния кно­пок. При­пи­сать к каж­дой один и тот же об­ра­бот­чик со­бы­тия на­жа­тия, нап­ри­мер but­ton_Click. Это де­ла­ет­ся в фай­ле Form2.De­sig­ner.cs в ре­ги­оне Win­dows Form De­sig­ner ge­ne­ra­ted co­de та­ким об­ра­зом:
    
    this.button1.Click += new System.Even­t­Han­d­ler(this.but­ton_Click);
    И так для каж­дой из шес­ти кно­пок.
    И опи­сать этот са­мый об­ра­бот­чик так:
    
    private vo­id but­ton_Click(obj­ect sen­der, Even­tArgs e) {
    int idx = Int32.Par­se(((But­ton)sen­der).Tag as string); // оп­ре­де­ля­ем к ка­кой фиш­ке от­но­сит­ся кноп­ка
    colorDialog1.Color = ((But­ton)sen­der).Bac­k­Co­lor; // ус­та­нав­ли­ва­ем цвет фиш­ки в ди­ало­ге
    if (co­lor­Di­alog1.Show­Di­alog(this) == Di­alog­Re­sult.OK) { // от­к­ры­ва­ем ди­алог
    ((Button)sender).BackColor = co­lor­Di­alog1.Co­lor; // ус­та­нав­ли­ва­ем выб­ран­ный цвет на кноп­ку
    pf.options.fishkaColors[idx] = co­lor­Di­alog1.Co­lor; // и на фиш­ку
    }
    }
    
    Так по­лу­ча­ет­ся од­на фун­к­ция, но чуть боль­ше дру­гой ра­бо­ты.
    
    Вот те­перь мож­но иг­рать. :)
    
    Желающие мо­гут по­эк­с­пе­ри­мен­ти­ро­вать с фор­ма­ми фи­шек. Мне прос­то бы­ло от­к­ро­вен­но лень вы­пи­сы­вать вся­кие то­ро­иды, пя­ти­уголь­ни­ки и про­чее. Го­раз­до ин­те­рес­нее эк­с­пе­ри­мен­ты с гра­ди­ен­т­ны­ми за­лив­ка­ми. Те, ко­му ин­те­рес­но - пос­мот­ри­те класс Blend, в ко­то­ром за­да­ют­ся рас­п­ре­де­ле­ние цве­тов для мно­жес­т­вен­но­го гра­ди­ен­та.
    Каталог про­ек­та для MS Vi­su­al Stu­dio 2005 с при­ме­ром на­хо­дит­ся в ар­хи­ве при­ме­ров, под­ка­та­лог puz­zle.
    
3. Самодельный изменятель размера картинок.
    
    На мно­гих фо­ру­мах/бло­гах/сай­тах есть ог­ра­ни­че­ния на заг­ру­жа­емые кар­тин­ки ти­па та­ких: "не боль­ше чем 1024 пик­се­лей по сто­ро­не и не боль­ше чем 1 Мб".
    
    Постановка за­да­чи - на­до соз­дать прог­рам­мку для мас­со­во­го умень­ше­ния раз­ме­ров и объ­ема кар­ти­нок по за­дан­ным па­ра­мет­рам. Бу­дем ис­хо­дить из то­го, что кар­тин­ки с ко­то­ры­ми пред­с­то­ит ра­бо­тать, на­хо­дят­ся в фор­ма­те jpeg или png, впро­чем gif, bmp и tiff - то­же под­хо­дят. Ес­ли фор­мат дру­гой - то при­дет­ся под­к­лю­чать биб­ли­оте­ку ра­бо­ты с этим фор­ма­том, а в худ­шем слу­чае, пи­сать свою. Пе­ре­чис­лен­ные 5 фор­ма­тов под­дер­жи­ва­ют­ся .NET, так что проб­лем не бу­дет.
    
    Нам по­на­до­бит­ся: воз­мож­ность до­бав­ле­ния/уда­ле­ния фай­лов/ка­та­ло­гов в спи­сок об­ра­бот­ки, за­да­ние па­ра­мет­ров (раз­ме­ра и объ­ема), вы­бор ва­ри­ан­та сох­ра­не­ния и мес­та сох­ра­не­ния. Прис­ту­па­ем - соз­да­ем ок­но, в ко­то­ром пре­дус­мат­ри­ва­ем все, что толь­ко что пе­ре­чис­ли­ли.
    
    Добавляем open­Fi­le­Di­alog, fol­der­B­row­ser­Di­alog и за­пол­ня­ем com­bo­Box1 зна­че­ни­ями JPEG, PNG, TIFF, BMP, GIF. Для open­Fi­le­Di­alog опи­сы­ва­ем свой­ст­во Fil­ter так - "Гра­фи­чес­кие фай­лы (*.jpg, *.png, *.tiff, *.gif, *.bmp)|*.jpg;*.jpe;*.jpeg;*.png;*.tif;*.tiff;*.bmp;*.gif||", а свой­ст­во Mul­ti­Se­lect = true. Для com­bo­Box1 ус­та­нав­ли­ва­ем свой­ст­во Drop­Dow­n­S­t­y­le = Drop­Dow­n­List. Для lis­t­Box1 ус­та­нав­ли­ва­ем свой­ст­во Ho­ri­zon­tal­S­c­rol­lbar = true
    
    Первое приб­ли­же­ние
    Описываем фун­к­цию кноп­ки До­ба­вить фай­лы:
    
    private vo­id but­ton1_Click(obj­ect sen­der, Even­tArgs e) {
    if (open­Fi­le­Di­alog1.Show­Di­alog(this) == Di­alog­Re­sult.OK) { // от­к­рыть ди­алог вы­бо­ра фай­лов
    listBox1.Items.AddRange(openFileDialog1.FileNames); // за­пол­ня­ем lis­t­box име­на­ми выб­ран­ных фай­лов
    }
    }
    
    Запускаем про­гу и смот­рим что по­лу­чи­лось. Пос­ле вы­бо­ра фай­лов ви­дим, что в lis­t­box они пи­шут­ся с пол­ным пу­тем... не очень удоб­но чи­тать. Как бы сде­лать так, что­бы пи­са­лись толь­ко име­на фай­лов, но за­по­ми­на­лись они пол­нос­тью?
    Создаем класс, ко­то­рый бу­дет со­дер­жать пол­ный путь и толь­ко имя фай­ла, и воз­в­ра­щать в lis­t­box толь­ко ко­рот­кое имя:
    
    internal class Fi­le­Lis­tI­tem
    {
    internal string ful­lFi­le­na­me; // пол­ное имя фай­ла
    internal string shor­t­Fi­le­na­me; // ко­рот­кое имя фай­ла
    internal Fi­le­Lis­tI­tem() { // кон­с­т­рук­тор
    fullFilename = "";
    shortFilename = "";
    }
    internal Fi­le­Lis­tI­tem(string in­Full, string in­S­hort) { // кон­с­т­рук­тор со все­ми па­ра­мет­ра­ми
    fullFilename = in­Full;
    shortFilename = in­S­hort;
    }
    internal Fi­le­Lis­tI­tem(string in­Full) { // кон­с­т­рук­тор толь­ко с пол­ным име­нем
    fullFilename = in­Full;
    shortFilename = Path.Get­Fi­le­Na­me(inFull);
    }
    public over­ri­de string ToS­t­ring() { // фун­к­ция, к ко­то­рой об­ра­ща­ет­ся lis­t­box при за­пол­не­нии спис­ка
    return shor­t­Fi­le­na­me;
    }
    }
    
    Теперь из­ме­ним фун­к­цию об­ра­бот­ки на­жа­тия кноп­ки на та­кую:
    
    for (int i = 0; i ‹ open­Fi­le­Di­alog1.Fi­le­Na­mes.Length; i++) { // для каж­до­го из выб­ран­ных фай­лов
    listBox1.Items.Add(new Fi­le­Lis­tI­tem(open­Fi­le­Di­alog1.Fi­le­Na­mes[i])); // до­ба­вить в lis­t­box соз­дан­ный объ­ект Fi­le­Lis­tI­tem
    }
    
    Запускаем и смот­рим, что по­лу­чи­лось. Вро­де нор­маль­но.
    
    Дальше пи­шем фун­к­цию кноп­ки До­ба­вить ка­та­лог:
    
    private vo­id but­ton2_Click(obj­ect sen­der, Even­tArgs e) {
    if (fol­der­B­row­ser­Di­alog1.Show­Di­alog(this) == Di­alog­Re­sult.OK) { // от­к­ры­ва­ем ди­алог вы­бо­ра ка­та­ло­га
    DirectoryInfo di = new Di­rec­tor­yIn­fo(fol­der­B­row­ser­Di­alog1.Se­lec­ted­Path); // соз­да­ем объ­ект ин­фор­ма­ции о выб­ран­ном ка­та­ло­ге
    foreach (Fi­le­In­fo fi in di.Get­Fi­les("*.jpg")) { // для каж­до­го фай­ла jpg
    listBox1.Items.Add(new Fi­le­Lis­tI­tem(fi.Ful­lNa­me)); // до­ба­вить имя фай­ла в спи­сок
    }
    //аналогичный цикл для каж­до­го рас­ши­ре­ния
    }
    }
    
    Можно ис­поль­зо­вать цикл для каж­до­го фай­ла и внут­ри цик­ла про­ве­рять рас­ши­ре­ние фай­ла, нап­ря­мую или че­рез Re­gex:
    
    foreach (Fi­le­In­fo fi in di.Get­Fi­les("*.*")) { // для каж­до­го фай­ла в ка­та­ло­ге
    if (fi.Exten­si­on.Equ­als(".jpg") || fi.Exten­si­on.Equ­als(".jpe") || fi.Exten­si­on.Equ­als(".jpeg") || fi.Exten­si­on.Equ­als(".png") || fi.Exten­si­on.Equ­als(".tif") || fi.Exten­si­on.Equ­als(".tiff") || fi.Exten­si­on.Equ­als(".gif") || fi.Exten­si­on.Equ­als(".bmp")) { // ес­ли рас­ши­ре­ние нам под­хо­дит
    listBox1.Items.Add(new Fi­le­Lis­tI­tem(fi.Ful­lNa­me)); // до­ба­вить в спи­сок
    }
    }
    
    Запускаем, про­ве­рям. Ра­бо­та­ет, но прог­рам­ма не прос­мат­ри­ва­ет под­ка­та­ло­ги. На­до поп­ра­вить. Для это­го:
    выносим код до­бав­ле­ния фай­лов в от­дель­ную фун­к­цию:
    
    
    internal vo­id Ad­dFi­les­F­rom­Dir(string dir) {
    DirectoryInfo di = new Di­rec­tor­yIn­fo(dir);
    foreach (Fi­le­In­fo fi in di.Get­Fi­les("*.*")) {
    if (fi.Exten­si­on.Equ­als(".jpg") || fi.Exten­si­on.Equ­als(".jpe") || fi.Exten­si­on.Equ­als(".jpeg") || fi.Exten­si­on.Equ­als(".png") || fi.Exten­si­on.Equ­als(".tif") || fi.Exten­si­on.Equ­als(".tiff") || fi.Exten­si­on.Equ­als(".gif") || fi.Exten­si­on.Equ­als(".bmp")) {
    listBox1.Items.Add(new Fi­le­Lis­tI­tem(fi.Ful­lNa­me));
    }
    }
    }
    
    меняем функцию обработки кнопки Добавить каталог:
    
    if (fol­der­B­row­ser­Di­alog1.Show­Di­alog(this) == Di­alog­Re­sult.OK) {
    AddFilesFromDir(folderBrowserDialog1.SelectedPath);
    }
    и до­бав­ля­ем в фун­к­цию до­бав­ле­ния фай­лов ре­кур­сив­ный код, в ко­нец фун­к­ции:
    
    foreach (Di­rec­tor­yIn­fo di2 in di.Get­Di­rec­to­ri­es()) { // для каж­до­го под­ка­та­ло­га в ка­та­ло­ге di
    AddFilesFromDir(di2.FullName); // до­ба­вить фай­лы из не­го
    }
    
    Запускаем, про­ве­ря­ем. Вро­де ра­бо­та­ет. Не­удоб­но каж­дый раз в ок­не вы­бо­ра ка­та­ло­га пры­гать по длин­но­му де­ре­ву... на­до бы поп­ра­вить... В фун­к­цию об­ра­бот­ки кноп­ки До­ба­вить ка­та­лог до­ба­вим пер­вой строч­кой­:
    
    if (fol­der­B­row­ser­Di­alog1.Se­lec­ted­Path == "") { fol­der­B­row­ser­Di­alog1.Se­lec­ted­Path = @"C:\"; }
    Ну, путь пи­ше­те тот, ко­то­рый вам удо­бен.
    
    Пишем фун­к­цию об­ра­бот­ки кноп­ки Уб­рать вы­де­лен­ное:
    
    private vo­id but­ton3_Click(obj­ect sen­der, Even­tArgs e) {
    if (lis­t­Box1.Se­lec­te­dIn­dex != -1) { // ес­ли что-то вы­де­лен­но
    listBox1.Items.RemoveAt(listBox1.SelectedIndex); // уб­рать вы­де­лен­ный эле­мент
    }
    }
    Дальше опи­сы­ва­ем кноп­ку Выб­рать для ка­та­ло­га сох­ра­не­ния:
    
    private vo­id but­ton4_Click(obj­ect sen­der, Even­tArgs e) {
    if (fol­der­B­row­ser­Di­alog1.Show­Di­alog(this) == Di­alog­Re­sult.OK) { // от­к­ры­ва­ем ди­алог вы­бо­ра ка­та­ло­га
    textBox4.Text = fol­der­B­row­ser­Di­alog1.Se­lec­ted­Path; // за­пи­сы­ва­ем выб­ран­ный путь в тек­с­то­вое по­ле
    }
    }
    Запускаем, про­ве­рям. За­ме­ча­ем, что в вы­па­да­ющем окош­ке Фор­мат сох­ра­не­ния ни­че­го не вы­би­ра­ет­ся са­мо... не­по­ря­док. Соз­да­дим об­ра­бот­чик со­бы­тия Form1_Lo­ad и впи­шем ту­да та­кой код:
    
    comboBox1.SelectedIndex = 0; // выб­рать пер­вый в спис­ке эле­мент
    Осталось са­мое глав­ное - на­пи­сать ос­нов­ные фун­к­ции. Пос­коль­ку в фор­ме хва­та­ет па­ра­мет­ров, вво­ди­мых поль­зо­ва­те­лем, луч­ше соз­дать от­дель­ную фун­к­цию про­вер­ки пра­виль­нос­ти вве­ден­ных дан­ных. Ес­тес­т­вен­но, фун­к­ция пре­об­ра­зо­ва­ния кар­тин­ки то­же дол­ж­на быть от­дель­ной. Та­ким об­ра­зом, об­ра­бот­чик кноп­ки Умень­шить бу­дет при­мер­но та­ким:
    
    private vo­id but­ton5_Click(obj­ect sen­der, Even­tArgs e) {
    if (!Chec­kIn­put()) { re­turn; }
    DoJob();
    }
    Теперь опи­сы­ва­ем про­вер­ку вве­ден­ных дан­ных. Что на­до про­ве­рять: на­ли­чие фай­лов про­ве­рять не на­до, это бу­дет де­лать фун­к­ция, ко­то­рая бу­дет с ни­ми ра­бо­тать, по­то­му что все выб­ран­ные фай­лы су­щес­т­во­ва­ли на мо­мент вы­бо­ра, а зна­чит они мо­гут ис­чез­нуть толь­ко по ошиб­ке, ко­то­рая мо­жет про­изой­ти ког­да-угод­но, и про­ве­рять ра­бо­тос­по­соб­ность фай­ла на­до пря­мо пе­ред (или во вре­мя) об­ра­ще­ния к не­му. На­до про­ве­рить вве­ден­ные чис­ла - что­бы сос­то­яли толь­ко из цифр, и не бы­ли слиш­ком боль­ши­ми/ма­лень­ки­ми. А еще на­до про­ве­рить ка­та­лог сох­ра­не­ния - ес­ли его нет, на­до соз­дать. За­од­но, соз­да­дим гло­баль­ные пе­ре­мен­ные, в ко­то­рых бу­дем хра­нить вве­ден­ные дан­ные.
    
    internal int width; //ши­ри­на
    internal int he­ight; //вы­со­та
    internal int si­ze; // объ­ем в бай­тах
    internal string res­dir; // ка­та­лог для ре­зуль­та­тов
    internal int fi­le­sEr­ror­Co­unt; // счет­чик оши­бок
    internal System.Dra­wing.Ima­ging.Ima­ge­For­mat ima­ge­For­mat; // выб­ран­ный фор­мат фай­лов
    internal string ima­ge­Ext; // рас­ши­ре­ние выб­ран­но­го фор­ма­та
    internal bo­ol Chec­kIn­put() {
    try { // поп­ро­бо­вать
    width = Int32.Par­se(tex­t­Box1.Text); // прев­ра­тить вве­де­ное в тек­с­то­вом по­ле в це­лое чис­ло
    if (width ‹= 1) { re­turn fal­se; } // ес­ли ши­ри­на слиш­ком ма­лень­кая - вер­нуть ошиб­ку
    }
    catch { // ес­ли что-то не по­лу­чи­лось
    return fal­se; // вер­нуть ошиб­ку
    }
    try {
    height = Int32.Par­se(tex­t­Box2.Text);
    if (he­ight ‹= 1) { re­turn fal­se; } // ес­ли вы­со­та слиш­ком ма­лень­кая - вер­нуть ошиб­ку
    }
    catch {
    return fal­se;
    }
    try {
    size = Int32.Par­se(tex­t­Box3.Text);
    size *= 1024; // пе­ре­во­дим в бай­ты
    if (si­ze == 0) { re­turn fal­se; } // ес­ли объ­ем ра­вен 0 - вер­нуть ошиб­ку
    }
    catch {
    return fal­se;
    }
    if (tex­t­Box4.Text.Length == 0) { re­turn fal­se; } // ес­ли путь не вве­ден - ошиб­ка
    resdir = tex­t­Box4.Text;
    if (res­dir[res­dir.Length - 1] != '') { res­dir += ""; } // уточ­ня­ем на­ли­чие сле­ша на кон­це пу­ти
    if (!Di­rec­tory.Exis­ts(res­dir)) { // ес­ли ка­та­лог не су­щес­т­ву­ет
    try { // поп­ро­бо­вать
    resdir = Di­rec­tory.Cre­ate­Di­rec­tory(res­dir).Ful­lNa­me; // соз­дать ка­та­лог, за­пи­сать пол­ный путь в res­dir и до­ба­вить слеш
    }
    catch {
    return fal­se;
    }
    }
    switch (com­bo­Box1.Se­lec­te­dIn­dex) { // в за­ви­си­мос­ти от то­го, что выб­ра­но в окош­ке Фор­мат
    case 0:
    imageFormat = System.Dra­wing.Ima­ging.Ima­ge­For­mat.Jpeg; // ус­та­но­вить фор­мат фай­лов
    imageExt = ".jpg"; // и рас­ши­ре­ние
    break;
    case 1:
    imageFormat = System.Dra­wing.Ima­ging.Ima­ge­For­mat.Png;
    imageExt = ".png";
    break;
    case 2:
    imageFormat = System.Dra­wing.Ima­ging.Ima­ge­For­mat.Tiff;
    imageExt = ".tif";
    break;
    case 3:
    imageFormat = System.Dra­wing.Ima­ging.Ima­ge­For­mat.Bmp;
    imageExt = ".bmp";
    break;
    case 4:
    imageFormat = System.Dra­wing.Ima­ging.Ima­ge­For­mat.Gif;
    imageExt = ".gif";
    break;
    }
    return true;
    }
    Желающие мо­гут пе­ред каж­дым re­turn fal­se вста­вить со­об­ще­ние об ошиб­ке, ли­бо прос­то:
    
    MessageBox.Show(this, "Неп­ра­виль­но вве­де­на ши­ри­на", "Ошиб­ка");
    либо сде­лать от­дель­ную фун­к­цию по­ка­за оши­бок.
    
    Переходим к глав­но­му - фун­к­ции умень­ше­ния. Итак, фун­к­ция дол­ж­на от­к­рыть файл, пос­мот­реть по­па­да­ет ли он под вве­ден­ные па­ра­мет­ры. Ес­ли да - прос­то ско­пи­ро­вать его, ес­ли нет - то пе­ре­сох­ра­нить его. При­чем, сна­ча­ла на­до по­дог­нать раз­ме­ры, оп­ре­де­лив ка­кая из сто­рон боль­ше выс­ту­па­ет за раз­ре­шен­ное зна­че­ние, поп­ро­бо­вать сох­ра­нить, пос­мот­реть объ­ем - и пос­ле­до­ва­тель­но про­пор­ци­ональ­но умень­шать раз­мер, по­ка объ­ем не ста­нет нуж­ным. Же­ла­ющим эк­с­пе­ри­мен­ти­ро­вать со сте­пенью сжа­тия для из­ме­не­ия объ­ема, что бо­лее ра­зум­но во мно­гих слу­ча­ях, при­дет­ся пой­ти чуть дру­гим пу­тем - при сох­ра­не­нии кар­тин­ки пе­ре­да­вать фун­к­ции Sa­ve объ­ект клас­са En­co­der­Pa­ra­me­ters с ус­та­нов­лен­ной сте­пенью сжа­тия. Сде­ла­ем од­ну фун­к­цию уп­рав­ля­ющей­, а про­вер­ку и все опе­ра­ции вы­не­сем в от­дель­ный фун­к­ции.
    
    internal vo­id Do­J­ob() {
    filesErrorCount = 0;
    int ima­geC­heck = 0;
    for (int i = 0; i ‹ lis­t­Box1.Items.Co­unt; i++) { // для каж­до­го эле­мен­та спис­ка
    imageCheck = IsI­ma­ge­Ok(((Fi­le­Lis­tI­tem)lis­t­Box1.Items[i]).ful­lFi­le­na­me); // про­ве­ря­ем файл
    if (ima­geC­heck == 0) { // ес­ли оши­бок нет
    File.Copy(((FileListItem)listBox1.Items[i]).fullFilename, res­dir + ((Fi­le­Lis­tI­tem)lis­t­Box1.Items[i]).shor­t­Fi­le­na­me); // ско­пи­ро­вать файл
    continue;
    }
    else if (ima­geC­heck == 4) { con­ti­nue; } // ес­ли ошиб­ка от­к­ры­тия фай­ла - про­пус­тить
    else if (ima­geC­heck == 1) { // ес­ли раз­мер и фор­мат в по­ряд­ке
    SmallerImage(((FileListItem)listBox1.Items[i]).fullFilename, fal­se); // умень­шить объ­ем
    }
    else if (ima­geC­heck == 2) { // ес­ли раз­мер боль­ше
    ResizeImage(((FileListItem)listBox1.Items[i]).fullFilename); // умень­шить кар­тин­ку
    }
    else if (ima­geC­heck == 3) { // ес­ли раз­мер в по­ряд­ке, но фор­мат дру­гой
    ConvertImage(((FileListItem)listBox1.Items[i]).fullFilename); // из­ме­нить фор­мат
    }
    }
    // со­об­щить сколь­ко фай­лов прош­ло, сколь­ко вы­да­ли ошиб­ку
    MessageBox.Show(this, String.For­mat("Успеш­но пре­об­ра­зо­ва­но {0} фай­лов.nПро­изош­ли ошиб­ки при ра­бо­те с {1} фай­ла­ми.", lis­t­Box1.Items.Co­unt-fi­le­sEr­ror­Co­unt, fi­le­sEr­ror­Co­unt), "Ошиб­ка");
    }
    
    internal int IsI­ma­ge­Ok(string fn) { // про­вер­ка фай­лов
    Bitmap bmp;
    try { // поп­ро­бо­вать
    bmp = new Bit­map(fn); // соз­дать кар­тин­ку из фай­ла
    }
    catch { // ес­ли не по­лу­чи­лось
    filesErrorCount++; // пос­чи­тать ошиб­ку
    return 4; // вер­нуть ошиб­ку от­к­ры­тия
    }
    if (bmp.Width › width || bmp.He­ight › he­ight) { re­turn 2; } // ес­ли раз­мер боль­ше, вер­нуть 2
    bmp.Dispose(); // уб­рать кар­тин­ку из па­мя­ти
    if (!Path.Ge­tEx­ten­si­on(fn).To­Lo­wer().Equ­als(ima­ge­Ext)) { re­turn 3; } // ес­ли рас­ши­ре­ние не то, ко­то­рое на­до, вер­нуть 3
    FileInfo fi = new Fi­le­In­fo(fn); // соз­дать объ­ект ин­фор­ма­ции о фай­ле
    if (fi.Length › si­ze) { re­turn 1; } // ес­ли раз­мер фай­ла боль­ше, вер­нуть 1
    return 0; // оши­бок нет
    }
    
    internal vo­id Re­si­ze­Ima­ge(string fn) {
    int ne­wI­ma­ge­Width = 0;
    int ne­wI­ma­ge­He­ight = 0;
    Bitmap bmp;
    try {
    bmp = new Bit­map(fn);
    }
    catch {
    filesErrorCount++;
    return;
    }
    if ((do­ub­le)width / bmp.Width ‹ (do­ub­le)he­ight / bmp.He­ight) { { // ес­ли ши­ри­на боль­ше выс­ту­па­ет за гра­ни­цы, чем вы­со­та
    newImageWidth = width; // ши­ри­ну на мак­си­мум
    newImageHeight = (int)(bmp.He­ight * ((do­ub­le)width / bmp.Width)); // вы­со­ту про­пор­ци­ональ­но
    }
    else { // ес­ли на­обо­рот, вы­со­та боль­ше выс­ту­па­ет
    newImageHeight = he­ight; // вы­со­ту на мак­си­мум
    newImageWidth = (int)(bmp.Width * ((do­ub­le)he­ight / bmp.He­ight)); // ши­ри­ну про­пор­ци­ональ­но
    }
    bmp = new Bit­map(bmp, ne­wI­ma­ge­Width, ne­wI­ma­ge­He­ight); // соз­дать из­ме­нен­ное изоб­ра­же­ние
    try { // поп­ро­бо­вать
    bmp.Save(resdir + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(fn) + ima­ge­Ext, ima­ge­For­mat); // сох­ра­нить в нуж­ном фор­ма­те
    }
    catch {
    filesErrorCount++;
    return;
    }
    finally { // при лю­бом рас­к­ла­де
    bmp.Dispose(); // унич­то­жить объ­ект кар­тин­ки
    }
    FileInfo fi = new Fi­le­In­fo(res­dir + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(fn) + ima­ge­Ext); // ин­фор­ма­ция о но­вом фай­ле
    if (fi.Length › si­ze) { // ес­ли объ­ем боль­ше
    SmallerImage(resdir + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(fn) + ima­ge­Ext, true); // умень­шить объ­ем
    }
    }
    
    В сле­ду­ющей фун­к­ции вро­де все дол­ж­но быть по­нят­но, все уже бы­ло от­ком­мен­ти­ро­ва­но.
    
    internal vo­id Con­ver­tI­ma­ge(string fn) {
    Bitmap bmp;
    try {
    bmp = new Bit­map(fn);
    }
    catch {
    filesErrorCount++;
    return;
    }
    try {
    bmp.Save(resdir + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(fn) + ima­ge­Ext, ima­ge­For­mat);
    }
    catch {
    filesErrorCount++;
    return;
    }
    finally {
    bmp.Dispose();
    }
    FileInfo fi = new Fi­le­In­fo(res­dir + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(fn) + ima­ge­Ext);
    if (fi.Length › si­ze) {
    SmallerImage(resdir + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(fn) + ima­ge­Ext, true);
    }
    }
    
    Уменьшаем объ­ем. Я по­шел са­мым прос­тым пу­тем - пус­тил в цикл умень­ше­ние на 5% по раз­ме­ру, по­ка объ­ем не ста­нет нуж­ным.
    Входящий па­ра­метр in­p­la­ce - по­ка­зы­ва­ет что ме­ня­ем - ста­рый файл с ко­пи­ро­ва­ни­ем, или но­вый в сво­ем мес­те.
    
    internal vo­id Smal­le­rI­ma­ge(string fn, bo­ol in­p­la­ce) {
    bool do­ne = fal­se; // флаг пра­виль­нос­ти объ­ема
    string resfn; // имя ре­зуль­ти­ру­юще­го фай­ла
    if (inpla­ce) { resfn = fn; } // ес­ли на мес­те - ре­зуль­ти­ру­ющее имя рав­но ис­ход­но­му
    else { resfn = res­dir + Path.Get­Fi­le­Na­me­Wit­ho­utEx­ten­si­on(fn) + ima­ge­Ext; } // ес­ли нет - соз­да­ем ре­зуль­ти­ру­ющее
    Bitmap bmp, bmp2;
    while (!do­ne) { // по­ка раз­мер не по­дой­дет
    try {
    bmp = new Bit­map(fn);
    }
    catch {
    filesErrorCount++;
    return;
    }
    bmp2 = new Bit­map(bmp, (int)(bmp.Width * 0.95), (int)(bmp.He­ight * 0.95)); // соз­да­ем но­вую кар­тин­ку на 5% мень­ше
    bmp.Dispose(); // унич­то­жа­ем ста­рую кар­тин­ку
    try {// поп­ро­бо­вать
    if (inpla­ce) { // ес­ли на мес­те
    File.Delete(resfn); // уда­лить файл, на мес­то ко­то­ро­го бу­дем сей­час пи­сать
    }
    bmp2.Save(resfn, ima­ge­For­mat); // сох­ра­ня­ем но­вую кар­тин­ку
    }
    catch {
    filesErrorCount++;
    return;
    }
    finally {
    bmp2.Dispose(); // в лю­бом слу­чае - унич­то­жа­ем но­вую кар­тин­ку
    }
    FileInfo fi = new Fi­le­In­fo(resfn); // ин­фор­ма­ция о но­вом фай­ле
    if (fi.Length ‹= si­ze) { // ес­ли раз­мер под­хо­дит
    done = true; // ста­вим флаг
    }
    fn = resfn; // имя фай­ла те­перь точ­но рав­но ре­зуль­ти­ру­юще­му
    inplace = true; // и ра­бо­та­ем даль­ше на мес­те
    GC.Collect(); // очи­ща­ем па­мять от му­со­ра.
    }
    }
    Запускаем. Про­ве­ря­ем. Вро­де все ра­бо­та­ет... вот толь­ко по­нять это слож­но - кноп­ку на­жа­ли и про­га ви­сит, по­ка не за­кон­чит. На­до бы по-акку­рат­нее сде­лать, прог­ресс ин­ди­ка­тор, нап­ри­мер.
    
    Второе приб­ли­же­ние - или бе­зо­пас­ная муль­ти­по­точ­ность
    Добавим в фор­му ма­лень­кий прог­ресс ин­ди­ка­тор и ком­по­нент Bac­k­g­ro­un­d­Wor­ker, ко­то­рый по­явил­ся толь­ко в .NET 2.0. Это та­кой спе­ци­аль­ный ра­бот­ник, ко­то­рый сам се­бе ти­хо ра­бо­та­ет в фо­но­вом ре­жи­ме, не ме­ша­ет ос­нов­ной прог­рам­ме вы­пол­нять­ся и толь­ко со­об­ща­ет о сво­ем прог­рес­се, да ждет от­ме­ны. При­коль­ная фи­ча, рань­ше при­хо­ди­лось то же са­мое пи­сать лап­ка­ми, или ко­пи­ро­вать из про­ек­та в про­ект, а те­перь удоб­нее ста­ло и бе­зо­пас­нее. :)
    Для ком­по­нен­та bac­k­g­ro­un­d­Wor­ker1 оп­ре­де­лим все три дос­туп­ных со­бы­тия:
    
    private vo­id bac­k­g­ro­un­d­Wor­ker1_Do­Work(obj­ect sen­der, Do­Wor­kE­ven­tArgs e) { // за­пуск ра­бот­ни­ка
    DoJob(sender); // на­ша глав­ная фун­к­ция ра­бо­ты, как ар­гу­мент пе­ре­да­ем ей ука­за­тель на ра­бот­ни­ка
    }
    private vo­id bac­k­g­ro­un­d­Wor­ker1_Prog­res­sChan­ged(obj­ect sen­der, Prog­res­sChan­ge­dE­ven­tArgs e) { // из­ме­не­ние прог­рес­са
    progressBar1.Value = e.Prog­res­sPer­cen­ta­ge; // ус­та­но­вить зна­че­ние прог­ресс ин­ди­ка­то­ра
    }
    private vo­id bac­k­g­ro­un­d­Wor­ker1_Run­Wor­ker­Com­p­le­ted(obj­ect sen­der, Run­Wor­ker­Com­p­le­te­dE­ven­tArgs e) { // за­вер­ше­ние ра­бо­ты
    UseWaitCursor = fal­se; // от­ме­нить кур­сор ожи­да­ния (ча­си­ки)
    progressBar1.Value = 0; // снять прог­ресс
    button5.Enabled = true; // вклю­ча­ем кноп­ку Умень­шить
    }
    Поправим на­шу фун­к­цию ра­бо­ты, и об­ра­бот­чик со­бы­тия на­жа­тия кноп­ки Умень­шить:
    
    private vo­id but­ton5_Click(obj­ect sen­der, Even­tArgs e) {
    if (!Chec­kIn­put()) { re­turn; }
    UseWaitCursor = true; // вклю­ча­ем кур­сор ча­си­ки
    button5.Enabled = fal­se; // вык­лю­ча­ем кноп­ку умень­шить - чтоб не бы­ло вся­ких двой­ных кли­ков
    backgroundWorker1.RunWorkerAsync(); // за­пус­ка­ем ра­бот­ни­ка
    }
    internal vo­id Do­J­ob(obj­ect wor­ker) { // ос­нов­ная фун­к­ция те­перь с па­ра­мет­ром
    filesErrorCount = 0;
    int ima­geC­heck = 0;
    for (int i = 0; i ‹ lis­t­Box1.Items.Co­unt; i++) {
    imageCheck = IsI­ma­ge­Ok(((Fi­le­Lis­tI­tem)lis­t­Box1.Items[i]).ful­lFi­le­na­me);
    if (ima­geC­heck == 0) { Fi­le.Copy(((Fi­le­Lis­tI­tem)lis­t­Box1.Items[i]).ful­lFi­le­na­me, res­dir + ((Fi­le­Lis­tI­tem)lis­t­Box1.Items[i]).shor­t­Fi­le­na­me); con­ti­nue; }
    else if (ima­geC­heck == 4) { con­ti­nue; }
    else if (ima­geC­heck == 1) {
    SmallerImage(((FileListItem)listBox1.Items[i]).fullFilename, fal­se);
    }
    else if (ima­geC­heck == 2) {
    ResizeImage(((FileListItem)listBox1.Items[i]).fullFilename);
    }
    else if (ima­geC­heck == 3) {
    ConvertImage(((FileListItem)listBox1.Items[i]).fullFilename);
    }
    ((BackgroundWorker)worker).ReportProgress((int)Math.Round((double)i * 100/ lis­t­Box1.Items.Co­unt)); // уточ­нить прог­ресс
    }
    // в вы­зо­ве ок­на уби­ра­ем при­вяз­ку к фор­ме
    MessageBox.Show(String.Format("Успешно пре­об­ра­зо­ва­но {0} фай­лов.nПро­изош­ли ошиб­ки при ра­бо­те с {1} фай­ла­ми.", lis­t­Box1.Items.Co­unt-fi­le­sEr­ror­Co­unt, fi­le­sEr­ror­Co­unt), "Ошиб­ка");
    }
    Вот и все. Ко­неч­но, по уму, на­до бы еще пе­ред за­пус­ком ра­бот­ни­ка вык­лю­чать все эле­мен­ты уп­рав­ле­ния и вклю­чать их по за­вер­ше­нии. А еще мож­но кноп­ку От­ме­на вста­вить.
    Каталог про­ек­та для MS Vi­su­al Stu­dio 2005 с при­ме­ром на­хо­дит­ся в ар­хи­ве при­ме­ров, под­ка­та­лог ima­ge_re­si­zer.
    
4. Системный индикатор в заголовке окна.
    
    Работал тут с од­ной глюч­ной про­гой и мне по­на­до­бил­ся хо­ро­ший де­тек­тор сво­бод­но­го мес­та на хар­де. А что­бы его бы­ло всег­да вид­но и он не ме­шал ра­бо­те, я ре­шил его вы­вес­ти в за­го­ло­вок ак­тив­но­го ок­на. Те­перь ре­шил рас­ска­зать, как я это сде­лал и по­ка­зать, что еще мож­но ана­ло­гич­но вы­во­дить.
    
    Во-первых, соз­да­ем пус­тое, ма­лень­кое и не­ви­ди­мое ок­но. Для это­го на­до за­дать сле­ду­ющие свой­ст­ва соз­дан­ной фор­мы:
    
    ClientSize = new System.Dra­wing.Si­ze(86, 26); // ши­ри­ну под­бе­ри­те под раз­мер вы­во­ди­мо­го тек­с­та, вы­со­ту бу­дем ста­вить прог­рам­мно
    ControlBox = fal­se; // уби­ра­ем кноп­ки ми­ни­ми­за­ции и пр.
    Text = ""; // уби­ра­ем текст из за­го­лов­ка, что­бы за­го­ло­вок про­пал
    DoubleBuffered = true; // двой­ной бу­фер - чтоб мень­ше мер­ца­ло при ри­со­ва­нии
    FormBorderStyle = System.Win­dows.For­ms.For­m­Bor­der­S­t­y­le.No­ne; // уби­ра­ем гра­ни­цу ок­на
    ShowInTaskbar = fal­se; // не по­ка­зы­вать в па­не­ли за­дач
    TopMost = true; // са­мое вер­х­нее (вы­ше толь­ко Tas­k­Ma­na­ger)
    TransparencyKey = System.Dra­wing.System­Co­lors.Con­t­rol; // де­ла­ем ок­но всег­да проз­рач­ным
    Добавим в фор­му ком­по­нент ti­mer, что­бы кон­т­ро­ли­ро­вать час­то­ту с ко­то­рой ве­дет­ся про­вер­ка дис­ка, оп­ре­де­лим со­бы­тия Pa­int и Mo­use­Up для фор­мы и Tick для тай­ме­ра.
    Сделаем фун­к­цию, за­пи­сы­ва­ющую те­ку­щее сво­бод­ное мес­то на дис­ке в гло­баль­ную пе­ре­мен­ную:
    
    internal long spa­ce;
    internal vo­id Chec­k­Si­ze() {
    try {
    space = System.IO.Dri­ve­In­fo.Get­D­ri­ves()[1].Ava­ilab­leF­re­eS­pa­ce; // по­лу­чить сво­бод­ное мес­то на пер­вом драй­ве
    Refresh(); // об­но­вить ок­но
    }
    catch { // ес­ли бы­ла ошиб­ка
    timer1.Stop(); // ос­та­но­вить тай­мер
    Close(); // вый­ти
    }
    }
    Диски ну­ме­ру­ют­ся в по­ряд­ке букв, т.е. в мо­ем слу­чае, 0 - A, 1 - C, 2 - D... Ес­ли про­вер­ка не уда­лась - это оз­на­ча­ет сбой дис­ка, по­это­му сто­ит зак­ры­тие прог­рам­мы, что­бы не му­чить драйв, с ко­то­рым что-то слу­чи­лось.
    
    Теперь на­до сде­лать фун­к­цию пре­об­ра­зо­ва­ния раз­ме­ра в чи­та­емую стро­ку. Раз­мер воз­в­ра­ща­ет­ся в бай­тах, ме­ня он ин­те­ре­су­ет до со­той ме­га­бай­та, так что я сде­лал так:
    
    internal string Ma­ke­Re­adab­le­Si­ze(long si­ze) {
    double ret = (do­ub­le)si­ze / 1024; // при­во­дим к ки­ло­бай­там
    size /= 1024;
    if (si­ze == 0) { // ес­ли це­лых ки­ло­бай­т 0 - вер­нуть стро­ку в ки­ло­бай­тах до со­той
    return String.For­mat("{0:f2}Kb", ret);
    }
    ret = (do­ub­le)si­ze / 1024; // при­во­дим к ме­га­бай­там
    return String.For­mat("{0:f2}Mb", ret); // вер­нуть в ме­га­бай­тах до со­той
    }
    Теперь мож­но сде­лать фун­к­цию ри­со­ва­ния:
    
    private vo­id Form1_Pa­int(obj­ect sen­der, Pa­in­tE­ven­tArgs e) {
    e.Graphics.DrawString(MakeReadableSize(space), myFont, myBrush, 0, 0); // ри­су­ем стро­ку в ок­не
    }
    Надо бы оп­ре­де­лить и за­дать кисть и шрифт, ко­то­рым поль­зу­ем­ся для на­пи­са­ния стро­ки, да и вы­со­ту ок­на на­до бы пос­та­вить. Пи­шем все это в ини­ци­али­за­цию ок­на:
    
    internal Font myFont;
    internal So­lid­B­rush myBrush;
    public Form1() {
    InitializeComponent();
    myBrush = new So­lid­B­rush(System­Co­lors.Acti­ve­Cap­ti­on­Text); // кисть цве­та тек­с­та за­го­лов­ка ак­тив­но­го ок­на
    myFont = System­Fon­ts.Cap­ti­on­Font; // шрифт - та­кой же
    Height = Syste­mIn­for­ma­ti­on.Cap­ti­on­He­ight; // за­дать вы­со­ту ок­на ра­ную вы­со­те за­го­лов­ка ок­на
    timer1.Start(); // за­пус­тить тай­мер
    }
    Сделаем еще зак­ры­тие по пра­во­му щел­ч­ку:
    
    private vo­id Form1_Mo­use­Up(obj­ect sen­der, Mo­use­Even­tArgs e) {
    if (e.But­ton == Mo­use­But­tons.Right) { // ес­ли пра­вая кноп­ка
    timer1.Stop(); // ос­та­но­вить тай­мер
    Close(); // зак­рыть ок­но
    }
    }
    Теперь фун­к­ция тай­ме­ра. Ес­ли мы хо­тим про­ве­рять сос­то­яние дис­ка каж­дые 50 мил­ли­се­кунд, а по­ло­же­ние ок­на (что­бы оно всег­да ос­та­ва­лось в за­го­лов­ке) каж­дые 10, на­до сде­лать при­мер­но так - за­дать ин­тер­вал тай­ме­ра 10 и на­пи­сать та­кую фун­к­цию Tick:
    
    internal int ticks;
    private vo­id ti­mer1_Tick(obj­ect sen­der, Even­tArgs e) {
    ticks++;
    CheckLocation(); // про­ве­рить по­ло­же­ние ок­на
    if (ticks == 5) { // ес­ли 5 ти­ков прош­ло (50 мил­ли­се­кунд)
    ticks = 0; // об­ну­лить счет­чик
    CheckSize(); // про­ве­рить диск
    }
    }
    Осталось са­мое слож­ное - на­учить­ся оп­ре­де­лять по­ло­же­ние на­ше­го ок­на, что­бы оно по­па­да­ло на за­го­ло­вок ак­тив­но­го ок­на.
    Для это­го нам на­до оп­ре­де­лять те­ку­щее ак­тив­ное ок­но, его раз­ме­ры, ви­ди­мо оно или скры­то, а так­же на­до оп­ре­де­лять ок­но дес­к­то­па и его раз­ме­ры - на слу­чай­, ес­ли ак­тив­но­го ок­на нет. Для все­го это­го есть фун­к­ции в Win­dows API, в биб­ли­оте­ке user32.dll. Про­пи­сы­ва­ем их вы­зо­вы:
    
    [DllImport("user32.dll", Char­Set = Char­Set.Auto)]
    public sta­tic ex­tern IntPtr Get­Des­k­top­Win­dow(); // по­лу­чить ука­за­тель на ок­но дес­к­то­па
    [DllImport("user32.dll", Char­Set = Char­Set.Auto)]
    public sta­tic ex­tern IntPtr Get­Fo­reg­ro­un­d­Win­dow(); // по­лу­чить ука­за­тель на ак­тив­ное ок­но
    [DllImport("user32.dll", Char­Set = Char­Set.Auto)]
    public sta­tic ex­tern bo­ol Get­Win­dow­Rect(IntPtr lpHwnd, ref Rec­tan­g­le lpRect); // по­лу­чить раз­мер ок­на по ука­за­те­лю
    [DllImport("user32.dll", Char­Set = Char­Set.Auto)]
    public sta­tic ex­tern IntPtr Get­Win­dow(IntPtr lpHwnd, uint wCmd); // по­лу­чить ок­но (сле­ду­ющее, пре­ды­ду­щее, до­чер­нее...)
    [DllImport("user32.dll", Char­Set = Char­Set.Auto)]
    public sta­tic ex­tern bo­ol Is­Win­dow­Vi­sib­le(IntPtr lpHwnd); // ви­ди­мо ли ок­но?
    Не за­бы­ва­ем до­ба­вить в на­ча­ле ко­да
    
    using System.Run­ti­me.Inte­rop­Ser­vi­ces;
    И пи­шем фун­к­цию про­вер­ки и кор­рек­ти­ров­ки по­ло­же­ния:
    
    internal vo­id Chec­k­Lo­ca­ti­on() {
    Rectangle rect = new Rec­tan­g­le(); // раз­мер ок­на
    IntPtr des­k­top_hWnd = Get­Des­k­top­Win­dow(); // ука­за­тель на дек­топ
    if (des­k­top_hWnd == In­t­P­tr.Ze­ro) { re­turn; } // ес­ли дек­топ не оп­ре­де­лен - вер­нуть­ся
    IntPtr hWnd = Get­Fo­reg­ro­un­d­Win­dow(); // ука­за­тель на ак­тив­ное ок­но
    while (!IsWin­dow­Vi­sib­le(hWnd) hWnd != In­t­P­tr.Ze­ro) { // ес­ли оно не ви­ди­мо
    hWnd = Get­Win­dow(hWnd, 2); // про­ве­рять до­чер­ние ок­на, по­ка не надй­ем ви­ди­мое или по­ка они не кон­чать­ся
    }
    if (hWnd == In­t­P­tr.Ze­ro || hWnd == this.Han­d­le) { // ес­ли ок­но не най­де­но, или ак­тив­ное ок­но - на­ша прог­рам­ма
    if (Get­Win­dow­Rect(des­k­top_hWnd, ref rect)) { // оп­ре­де­лить раз­мер дес­к­то­па
    Left = rect.Width - Width; // рас­по­ло­жить спра­ва
    Top = rect.He­ight - He­ight-50; // вни­зу, над па­нелью за­дач
    }
    }
    else if (Get­Win­dow­Rect(hWnd, ref rect)) { // по­лу­чить раз­мер ак­тив­но­го
    if (rect.Left == rect.Width) { // ес­ли ок­но ну­ле­вой ши­ри­ны
    if (Get­Win­dow­Rect(des­k­top_hWnd, ref rect)) {
    Left = rect.Width - Width;
    Top = rect.He­ight - He­ight - 50;
    }
    }
    Left = rect.Width -Width - 46; // ус­та­но­вить спра­ва, ми­нус кноп­ки зак­ры­тия
    Top = rect.Top+5; // 5 пик­се­лей ни­же гра­ни­цы
    }
    }
    Обратите вни­ма­ние, что rect.Width - это на са­мом де­ле по­ло­же­ние пра­вой сто­ро­ны ок­на, а вов­се не ши­ри­на. По­че­му так про­ис­хо­дит - не знаю. Мо­жет я че­го не так сде­лал в вы­зо­ве фун­к­ций­, а мо­жет это ошиб­ка пре­об­ра­зо­ва­те­ля Mic­ro­soft из struct rect в class Rec­tan­g­le.
    
    Вот и все с прог­рам­мой. Же­ла­ющие вы­вес­ти что-то дру­гое в за­го­ло­вок мо­гут из­ме­нить фун­к­цию Chec­k­Si­ze и Ma­ke­Re­adab­le­Si­ze со­от­вет­с­т­вен­но.
    Ну, нап­ри­мер, сво­бод­ная фи­зи­чес­кая па­мять:
    надо дек­ла­ри­ро­вать струк­ту­ру ин­фор­ма­ции о па­мя­ти и фун­к­цию API:
    
    [DllImport("kernel32")]
    static ex­tern vo­id Glo­bal­Me­mor­y­S­ta­tus(ref ME­MOR­Y­S­TA­TUS buf);
    [StructLayout(LayoutKind.Sequential)]
    public struct ME­MOR­Y­S­TA­TUS
    {
    public uint dwLength;
    public uint dwMe­mor­y­Lo­ad;
    public uint dwTo­talPhys;
    public uint dwA­va­ilPhys;
    public uint dwTo­tal­Pa­ge­Fi­le;
    public uint dwA­va­il­Pa­ge­Fi­le;
    public uint dwTo­tal­Vir­tu­al;
    public uint dwA­va­il­Vir­tu­al;
    }
    И из­ме­нить фун­к­цию Chec­k­Si­ze:
    
    MEMORYSTATUS memSt = new ME­MOR­Y­S­TA­TUS();
    GlobalMemoryStatus(ref memSt);
    space = mem­St.dwA­va­ilPhys;
    Менять Ma­ke­Re­adab­le­Si­ze не нуж­но - па­мять в мме­га­бай­тах впол­не чи­та­бель­на :)
    
    Мой под­ход - про­вер­ка сос­то­яния по тай­ме­ру не сов­сем ве­рен, ибо бо­лее ре­сур­со­емок, не­же­ли под­лю­че­ние к со­об­ще­ни­ям Win­dows об из­ме­не­нии сос­то­яния окон и дис­ков. Дру­гое де­ло, что да­же моя про­га жрет ма­ло, а под­к­лю­че­ние к сис­тем­ным со­об­ще­ни­ям де­ло бо­лее труд­ное и дол­гое.
    
    Итого - прог­рам­ма ви­сит в за­го­лов­ке ак­тив­но­го ок­на, ес­ли он есть. Ес­ли нет - по раз­но­му. Иног­да по­ви­са­ет в пра­вом ниж­нем уг­лу эк­ра­на, иног­да пря­чет­ся где-то.
    Каталог про­ек­та для MS Vi­su­al Stu­dio 2005 с при­ме­ром на­хо­дит­ся в ар­хи­ве при­ме­ров, под­ка­та­лог hdd_in_cap­ti­on.
    
5. "Ошкуривание" программы.
    
    Методов "ошку­ри­ва­ния" мно­жес­т­во, на­чи­ная от обыч­ных тем и за­кан­чи­вая кон­с­т­рук­то­ра­ми внеш­не­го ви­да. Рас­смот­рим два при­ема - са­мый прос­той и са­мый слож­ный­, ос­таль­ные ме­то­ды, в об­щем, по­хо­жи на рас­смат­ри­ва­емые. Нем­нож­ко по дру­го­му ус­т­ро­ены сти­ли Win­dows, хо­тя по су­ти они то­же шкур­ка, прос­то чуть ина­че сде­лан­ная.
    
    В об­щем и це­лом, лю­бая шкур­ка сос­то­ит из на­бо­ра гра­фи­чес­ких фай­лов и фай­лов опи­са­ний. Фор­мат гра­фи­чес­ких фай­лов мо­жет быть лю­бым, в пре­де­лах лег­ко дос­туп­ных для чте­ния. В час­т­ных слу­ча­ях вы­бор гра­фи­чес­ко­го фор­ма­та мо­жет быть ог­ра­ни­чен не­об­хо­ди­мос­тью сох­ра­нять зна­че­ния цве­тов не­ис­ка­жен­ны­ми, что ис­к­лю­ча­ет jpeg. Фор­мат фай­ла опи­са­ния мо­жет быть так­же лю­бым - хоть тек­с­то­вым, хоть xml.
    Что ка­са­ет­ся спо­со­бов хра­не­ния шку­рок на дис­ке - тут сво­бо­да пол­ная. Са­мый прос­той ва­ри­ант - соз­дать ка­та­лог для шку­рок, в ко­то­ром для каж­дой шкур­ки соз­да­вать свой под­ка­та­лог, и пи­сать в не­го фай­лы. Файл опи­са­ния сох­ра­нять в тек­с­то­вом фор­ма­те. Имен­но та­ким спо­со­бом бу­ду поль­зо­вать­ся я в при­ме­рах. Же­ла­ющие мо­гут вмес­то под­ка­та­ло­гов для шку­рок ис­поль­зо­вать ар­хив-фай­лы или xml фай­лы или все-что-угод­но свое.
    
    Метод пер­вый
    Не сов­сем шкур­ки, ско­рее те­мы для сво­ей прог­рам­мы. Соз­да­ет­ся обыч­ное при­ло­же­ние, без под­дер­ж­ки Win­dows сти­лей­, но ис­поль­зу­емые цве­та и фо­но­вые кар­тин­ки для всех эле­мен­тов уп­рав­ле­ния бе­рут­ся из фай­лов. Так, или при­мер­но так, сде­ла­ны те­мы Flas­h­Get.
    Пример:
    обычное при­ло­же­ние из трех кно­пок. Од­на кноп­ка вклю­ча­ет стиль 1, дру­гая стиль 2, третья - вы­ход.
    в Prog­ram.cs в фун­к­ции Ma­in ком­мен­та­рим строч­ку
    
    //Application.EnableVisualStyles();
    если она есть (в Vi­su­al Stu­dio 2005 она про­пи­сы­ва­ет­ся ав­то­ма­ти­чес­ки, в 2003 - нет)
    
    Создаем класс для хра­не­ния те­мы:
    
    internal class Skin­Desc1
    {
    internal Ima­ge bgBut­ton1; // фо­но­вая кар­тин­ка для кноп­ки 1
    internal Ima­ge bgBut­ton2;// фо­но­вая кар­тин­ка для кноп­ки 2
    internal Ima­ge bgBut­ton3;// фо­но­вая кар­тин­ка для кноп­ки 3
    internal Ima­ge bgForm; // фо­но­вая кар­тин­ка для фор­мы
    internal Co­lor col­But­ton1; // цвет фо­на для кноп­ки 1
    internal Co­lor col­But­ton2;// цвет фо­на для кноп­ки 2
    internal Co­lor col­But­ton3;// цвет фо­на для кноп­ки 3
    internal Co­lor col­Form;// цвет фо­на для фор­мы
    }
    Разумеется, вы мо­же­те в класс про­пи­сать лю­бые гра­фи­чес­кие свой­ст­ва объ­ек­тов - цвет шриф­та, шрифт, стиль шриф­та, раз­мер бор­дю­ра и про­чее.
    
    В клас­се фор­мы опи­сы­ва­ем на­жа­тия кно­пок, и до­бав­ля­ем в кон­с­т­рук­тор заг­руз­ку тем и их при­ме­не­ние:
    
    public Form1() {
    InitializeComponent();
    LoadSkins1(); // заг­ру­зить те­мы
    ApplySkin1(0); // при­ме­нить те­му 1
    }
    private vo­id but­ton1_Click(obj­ect sen­der, Even­tArgs e) { // кноп­ка Стиль 1
    ApplySkin1(0); // при­ме­нить стиль 1
    }
    private vo­id but­ton2_Click(obj­ect sen­der, Even­tArgs e) { // кноп­ка Стиль 2
    ApplySkin1(1); // при­ме­нить стиль 2
    }
    private vo­id but­ton3_Click(obj­ect sen­der, Even­tArgs e) { // кноп­ка Вы­ход
    Close(); // зак­рыть
    }
    В на­шем слу­чае фай­лов шкур­ки бу­дет 5 - фо­но­вые кар­тин­ки для каж­дой кноп­ки и для фор­мы, и тек­с­то­вый файл в ко­то­ром за­пи­са­ны цве­та. Цве­та за­пи­са­ны в та­ком ви­де: 255,0,0, по од­но­му на строч­ку.
    
    Теперь фун­к­ция заг­руз­ки тем:
    
    internal vo­id Lo­ad­S­kins1() {
    DirectoryInfo di = new Di­rec­tor­yIn­fo(@"../../skins/"); // ка­та­лог со шкур­ка­ми
    skins = new Skin­Desc1[di.Get­Di­rec­to­ri­es().Length]; // ко­ли­чес­т­во шку­рок = ко­ли­чес­т­ву под­ка­та­ло­гов
    int i = 0;
    string colStr = "";
    string[] tsa;
    foreach (Di­rec­tor­yIn­fo di2 in di.Get­Di­rec­to­ri­es()) { // для каж­до­го под­ка­та­ло­га
    skins[i] = new Skin­Desc1(); // соз­дать шкур­ку
    StreamReader sr = new Stre­am­Re­ader(di2.Ful­lNa­me + "\\co­lors.txt"); // от­к­рыть файл цве­тов
    try {
    try { // счи­ты­ва­ем цве­та
    colStr = sr.Re­ad­Li­ne(); // строч­ка цве­та
    tsa = col­S­tr.Split(','); // раз­бить по за­пя­тым на зна­че­ния RGB
    skins[i].colButton1 = Co­lor.Fro­mAr­gb(Int32.Par­se(tsa[0]), Int32.Par­se(tsa[1]), Int32.Par­se(tsa[2])); // соз­дать цвет
    colStr = sr.Re­ad­Li­ne();
    tsa = col­S­tr.Split(',');
    skins[i].colButton2 = Co­lor.Fro­mAr­gb(Int32.Par­se(tsa[0]), Int32.Par­se(tsa[1]), Int32.Par­se(tsa[2]));
    colStr = sr.Re­ad­Li­ne();
    tsa = col­S­tr.Split(',');
    skins[i].colButton3 = Co­lor.Fro­mAr­gb(Int32.Par­se(tsa[0]), Int32.Par­se(tsa[1]), Int32.Par­se(tsa[2]));
    colStr = sr.Re­ad­Li­ne();
    tsa = col­S­tr.Split(',');
    skins[i].colForm = Co­lor.Fro­mAr­gb(Int32.Par­se(tsa[0]), Int32.Par­se(tsa[1]), Int32.Par­se(tsa[2]));
    }
    catch { }
    try { skins[i].bgBut­ton1 = new Bit­map(di2.Ful­lNa­me + "\\but­ton1.jpg"); } // заг­ру­зить кар­тин­ку, ес­ли есть
    catch { skins[i].bgBut­ton1 = null; } // ес­ли нет - пос­та­вить null
    try { skins[i].bgBut­ton2 = new Bit­map(di2.Ful­lNa­me + "\\but­ton2.jpg"); }
    catch { skins[i].bgBut­ton1 = null; }
    try { skins[i].bgBut­ton3 = new Bit­map(di2.Ful­lNa­me + "\\but­ton3.jpg"); }
    catch { skins[i].bgBut­ton1 = null; }
    try { skins[i].bgForm = new Bit­map(di2.Ful­lNa­me + "\\form.jpg"); }
    catch { skins[i].bgBut­ton1 = null; }
    }
    catch { }
    finally {
    sr.Close();
    }
    i++;
    }
    }
    И фун­к­ция при­ме­не­ния тем:
    
    internal vo­id Ap­ply­S­kin1(int idx) {
    button1.BackColor = skins[idx].col­But­ton1; // ус­та­но­вить цвет
    button2.BackColor = skins[idx].col­But­ton2;
    button3.BackColor = skins[idx].col­But­ton3;
    BackColor = skins[idx].col­Form;
    if (skins[idx].bgBut­ton1 != null) { but­ton1.Bac­k­g­ro­un­dI­ma­ge = skins[idx].bgBut­ton1; } // ес­ли кар­тин­ка заг­ру­же­на - ус­та­но­вить
    else { but­ton1.Bac­k­g­ro­un­dI­ma­ge = null; } // ес­ли нет - снять
    if (skins[idx].bgBut­ton2 != null) { but­ton2.Bac­k­g­ro­un­dI­ma­ge = skins[idx].bgBut­ton2; }
    else { but­ton2.Bac­k­g­ro­un­dI­ma­ge = null; }
    if (skins[idx].bgBut­ton3 != null) { but­ton3.Bac­k­g­ro­un­dI­ma­ge = skins[idx].bgBut­ton3; }
    else { but­ton3.Bac­k­g­ro­un­dI­ma­ge = null; }
    if (skins[idx].bgForm != null) { Bac­k­g­ro­un­dI­ma­ge = skins[idx].bgForm; }
    else { Bac­k­g­ro­un­dI­ma­ge = null; }
    }
    
  Запускаем, про­ве­ря­ем:
    
    
 
  
    Метод вто­рой
    Уже не шкур­ка, ско­рее кон­с­т­рук­тор внеш­не­го ви­да. В файл опи­са­ния шкур­ки вхо­дят не толь­ко гра­фи­чес­кие па­ра­мет­ры, но и раз­мер и по­ло­же­ние эле­мен­та в фор­ме. Для каж­до­го эле­мен­та име­ет­ся под­дер­ж­ка трех по­ло­же­ний - обыч­но­го, под­с­ве­чен­но­го и на­жа­то­го, плюс ко все­му, вклю­че­на проз­рач­ность. Так, или при­мер­но так, сде­ла­ны шкур­ки Wi­nAmp mo­dern и BSPla­yer.
    Прозрачность в Win­dows луч­ше все­го под­к­лю­чать так, как это де­ла­ет Mic­ro­soft - под­к­лю­ча­емые кар­тин­ки хра­нят­ся в фор­ма­те bmp (или лю­бом дру­гом не ис­ка­жа­ющем зна­че­ния цве­тов при сох­ра­не­нии), а проз­рач­ным объ­яв­ля­ет­ся один из цве­тов. Аль­фа-ка­нал в кар­тин­ках не ис­поль­зу­ет­ся.
    
    Структура шкур­ки при­мер­но та­кая: в тек­с­то­вом фай­ле опи­са­ны все па­ра­мет­ры фор­мы (раз­мер фор­мы и цвет, объ­яв­лен­ный проз­рач­ным) и все эле­мен­ты уп­рав­ле­ния, ко­то­рые дол­ж­ны быть на­ри­со­ва­ны. Для каж­до­го эле­мен­та уп­рав­ле­ния за­да­ют­ся сле­ду­ющие па­ра­мет­ры - тип, раз­мер, по­ло­же­ние в фор­ме, цвет фо­на, цвет шриф­та, шрифт, име­на фай­лов для трех по­ло­же­ний­, текст. Тип эле­мен­та уп­рав­ле­ния за­да­ет не толь­ко что это в прин­ци­пе (кноп­ка, пол­зу­нок и пр.), но и что это кон­к­рет­но (ка­кая имен­но кноп­ка).
    
    В на­шем слу­чае в прог­рам­ме все­го 3 раз­ных кноп­ки, по­это­му ти­пов эле­мен­та уп­рав­ле­ния бу­дет 4: по од­но­му на кноп­ку и еще один для pic­tu­re­Box. ко­то­рый бу­дет ос­нов­ным на­пол­ни­те­лем гра­фи­чес­ко­го со­дер­жа­ния фор­мы.
    
    Пример:
    Создаем фор­му и за­да­ем сле­ду­ющие свой­ст­ва:
    
    FormBorderStyle = No­ne;
    ControlBox = fal­se;
    Text = "";
    Создаем класс хра­не­ния шкур­ки:
    
    internal class Skin­Desc1
    {
    internal Co­lor tran­s­pa­ren­t­Key; // цвет объ­яв­лен­ный проз­рач­ным
    internal Si­ze for­m­Si­ze; // раз­мер фор­мы
    internal Ar­ray­List con­t­rols; // кол­лек­ция опи­са­ний эле­мен­тов уп­рав­ле­ния
    internal Skin­Desc1() { // кон­с­т­рук­тор
    controls = new Ar­ray­List();
    }
    }
    и класс опи­са­ния эле­мен­та уп­рав­ле­ния:
    
    internal class MyCon­t­rol
    {
    internal byte type; // тип
    internal Po­int Lo­ca­ti­on; // по­ло­же­ние
    internal Si­ze Si­ze; // раз­мер
    internal Ima­ge nor­mal; // кар­тин­ка нор­маль­но­го сос­то­яния
    internal Ima­ge hig­h­lig­h­ted; // кар­тин­ка под­с­ве­чен­но­го сос­то­яния
    internal Ima­ge pres­sed; // кар­тин­ка на­жа­то­го сос­то­яния
    internal Co­lor bgCo­lor; // цвет фо­на
    internal Co­lor fgCo­lor; // цвет шриф­та
    internal Font font; // шрифт
    internal string Text; // текст
    }
    Теперь в фор­ме соз­да­ем по­ля хра­не­ния шкур­ки:
    
    internal Skin­Desc1[] skins; // опи­са­ния шку­рок
    internal int cur­ren­t­S­kin=-1; // те­ку­щая шкур­ка
    и на­жа­тия кно­пок:
    
    private vo­id but­ton1_Click(obj­ect sen­der, Even­tArgs e) { // кноп­ка 1
    ApplySkin1(0); // при­ме­нить стиль 1
    }
    private vo­id but­ton2_Click(obj­ect sen­der, Even­tArgs e) { // кноп­ка 2
    ApplySkin1(1); // при­ме­нить стиль 2
    }
    private vo­id but­ton3_Click(obj­ect sen­der, Even­tArgs e) { // кноп­ка 3
    Close(); // вы­ход
    }
    Обратите вни­ма­ние - соз­да­ем толь­ко фун­к­ции на­жа­тия, са­мих кно­пок не соз­да­ем.
    
    Также в фор­ме на­до опи­сать фун­к­ции пе­ре­тас­ки­ва­ния ок­на, пос­коль­ку за­го­ло­вок мы от­к­лю­чи­ли. Пе­ре­тас­ки­вать мож­но бу­дет за лю­бую ви­ди­мую часть фор­мы, кро­ме кно­пок, т.е. в на­шем слу­чае за лю­бой pic­tu­re­Box.
    
    internal bo­ol drag­ging; // флаг та­щат или нет
    internal Po­int drag­S­tart; // точ­ка на­ча­ла пе­ре­тас­ки­ва­ния
    private vo­id drag_mo­use­Down(obj­ect sen­der, Mo­use­Even­tArgs e) { // мышь на­жа­та
    dragging = true; // вклю­чить пе­ре­тас­ки­ва­ние
    dragStart = e.Lo­ca­ti­on; // за­пом­нить где на­жа­ли
    }
    private vo­id drag_mo­use­Up(obj­ect sen­der, Mo­use­Even­tArgs e) { // мышь от­пу­ще­на
    dragging = fal­se; // вык­лю­чить пе­ре­тас­ки­ва­ние
    }
    private vo­id drag_mo­use­Mo­ve(obj­ect sen­der, Mo­use­Even­tArgs e) { // мышь под­ви­ну­ли
    if (drag­ging) { // ес­ли та­щат
    Left = Po­in­t­ToS­c­re­en(e.Lo­ca­ti­on).X - drag­S­tart.X; // пе­ре­мес­тить ок­но
    Top = Po­in­t­ToS­c­re­en(e.Lo­ca­ti­on).Y - drag­S­tart.Y;
    }
    }
    
    Ну и на­до опи­сать фун­к­ции из­ме­не­ния ос­то­яния кно­пок - под­с­вет­ку и на­жа­тие:
    
    private vo­id mo­use­En­ter(obj­ect sen­der, Even­tArgs e) { // мышь над эле­мен­том уп­рав­ле­ния
    if (cur­ren­t­S­kin == -1) { re­turn; } // ес­ли шкур­ка выб­ра­на
    int idx = (int)((Con­t­rol)sen­der).Tag; // по­лу­чить ин­декс эле­мен­та уп­рав­ле­ния в кол­лек­ции
    MyControl mc = ((MyCon­t­rol)skins[cur­ren­t­S­kin].con­t­rols[idx]); // ука­за­тель на опи­са­ние эле­мен­та уп­рав­ле­ния
    if (mc.hig­h­lig­h­ted != null) { // ес­ли кар­тин­ка под­с­ве­чен­но­го сос­то­яния есть
    if (mc.type == 0) { // ес­ли мышь над pic­tu­re­Box
    ((PictureBox)sender).Image = mc.hig­h­lig­h­ted; // ус­та­но­вить кар­тин­ку под­с­вет­ки
    }
    else { // ес­ли над кноп­кой
    ((Button)sender).Image = mc.hig­h­lig­h­ted; // ус­та­но­вить кар­тин­ку под­с­вет­ки
    }
    }
    }
    
    private vo­id mo­use­Le­ave(obj­ect sen­der, Even­tArgs e) { // ана­ло­гич­но - мышь уш­ла с эле­мен­та
    if (cur­ren­t­S­kin == -1) { re­turn; }
    int idx = (int)((Con­t­rol)sen­der).Tag;
    MyControl mc = ((MyCon­t­rol)skins[cur­ren­t­S­kin].con­t­rols[idx]);
    if (mc.nor­mal != null) {
    // про­вер­ка на тип эле­мен­та - при сме­не шкур­ки со­бы­тие бу­дет ид­ти от эле­мен­та из дру­гой шкур­ки
    // что вы­зо­вет ошиб­ку, по­это­му про­ве­ря­ем не толь­ко в кол­лек­ции, но и ре­аль­но
    if (mc.type == 0 sen­der.Get­T­y­pe().Equ­als(type­of(Pic­tu­re­Box))) {
    ((PictureBox)sender).Image = mc.nor­mal;
    }
    else if ((mc.type == 1 || mc.type == 2 || mc.type == 3) sen­der.Get­T­y­pe().Equ­als(type­of(But­ton))) {
    ((Button)sender).Image = mc.nor­mal;
    }
    }
    }
    
    private vo­id mo­use­Down(obj­ect sen­der, Mo­use­Even­tArgs e) { // ана­ло­гич­но мышь на­жа­та
    if (cur­ren­t­S­kin == -1) { re­turn; }
    int idx = (int)((Con­t­rol)sen­der).Tag;
    MyControl mc = ((MyCon­t­rol)skins[cur­ren­t­S­kin].con­t­rols[idx]);
    if (mc.pres­sed != null) {
    if (mc.type == 0) {
    ((PictureBox)sender).Image = mc.pres­sed;
    }
    else {
    ((Button)sender).Image = mc.pres­sed;
    }
    }
    }
    
    private vo­id mo­use­Up(obj­ect sen­der, Mo­use­Even­tArgs e) { // ана­ло­гич­но мышь от­пу­ще­на
    if (cur­ren­t­S­kin == -1) { re­turn; }
    int idx = (int)((Con­t­rol)sen­der).Tag;
    MyControl mc = ((MyCon­t­rol)skins[cur­ren­t­S­kin].con­t­rols[idx]);
    if (mc.nor­mal != null) {
    if (mc.type == 0 sen­der.Get­T­y­pe().Equ­als(type­of(Pic­tu­re­Box))) {
    ((PictureBox)sender).Image = mc.nor­mal;
    }
    else if ((mc.type == 1 || mc.type == 2 || mc.type == 3) sen­der.Get­T­y­pe().Equ­als(type­of(But­ton))) {
    ((Button)sender).Image = mc.nor­mal;
    }
    }
    }
    
    Теперь са­мое глав­ное - фун­к­ции заг­руз­ки шкур­ки:
    
    internal vo­id Lo­ad­S­kins1() {
    DirectoryInfo di = new Di­rec­tor­yIn­fo(@"../../skins/"); // ка­та­лог с шкур­ка­ми
    skins = new Skin­Desc1[di.Get­Di­rec­to­ri­es().Length]; // ко­ли­чес­т­во шку­рок = ко­ли­чес­т­ву под­ка­та­ло­гов
    int i = 0; // счет­чик
    string colStr = ""; // строч­ка
    string[] tsa, tsa2; // мас­сив стро­ко­вых па­ра­мет­ров
    foreach (Di­rec­tor­yIn­fo di2 in di.Get­Di­rec­to­ri­es()) { // для каж­до­го под­ка­та­ло­га
    skins[i] = new Skin­Desc1(); // соз­дать шкур­ку
    StreamReader sr = new Stre­am­Re­ader(di2.Ful­lNa­me + "\\skin.txt", System.Text.Enco­ding.Ge­tEn­co­ding(1251)); // от­к­рыть файл опи­са­ния
    try {
    colStr = sr.Re­ad­Li­ne(); // счи­тать строч­ку
    tsa = col­S­tr.Split(','); // раз­бить по за­пя­тым на RGB
    skins[i].transparentKey = Co­lor.Fro­mAr­gb(Int32.Par­se(tsa[0]), Int32.Par­se(tsa[1]), Int32.Par­se(tsa[2])); // за­пи­сать цвет проз­рач­нос­ти
    colStr = sr.Re­ad­Li­ne();
    tsa = col­S­tr.Split(',');
    skins[i].formSize = new Si­ze(Int32.Par­se(tsa[0]), Int32.Par­se(tsa[1])); // за­пи­сать раз­мер фор­мы
    MyControl mc; // объ­ект опи­са­ния эле­мен­та уп­рав­ле­ния
    while ((colStr = sr.Re­ad­Li­ne()) != "") { // по­ка строч­ки не кон­чать­ся
    tsa = col­S­tr.Split(';'); // раз­бить стро­ку по точ­ке с за­пя­той
    mc = new MyCon­t­rol(); // но­вый объ­ект опи­са­ния
    mc.type = Byte.Par­se(tsa[0]); // за­пи­сать тип
    tsa2 = tsa[1].Split(',');
    mc.Location = new Po­int(Int32.Par­se(tsa2[0]), Int32.Par­se(tsa2[1])); // за­пи­сать по­ло­же­ние
    tsa2 = tsa[2].Split(',');
    mc.Size = new Si­ze(Int32.Par­se(tsa2[0]), Int32.Par­se(tsa2[1])); // за­пи­сать раз­мер
    tsa2 = tsa[3].Split(',');
    mc.bgColor = Co­lor.Fro­mAr­gb(Int32.Par­se(tsa2[0]), Int32.Par­se(tsa2[1]), Int32.Par­se(tsa2[2])); // за­пи­сать цвет фо­на
    tsa2 = tsa[4].Split(',');
    mc.fgColor = Co­lor.Fro­mAr­gb(Int32.Par­se(tsa2[0]), Int32.Par­se(tsa2[1]), Int32.Par­se(tsa2[2])); // за­пи­сать цвет шриф­та
    try { mc.nor­mal = new Bit­map(di2.Ful­lNa­me + "\" + tsa[5]); } // заг­ру­зить кар­тин­ку нор­маль­но­го сос­то­яния
    catch { mc.nor­mal = null; } // ес­ли не по­лу­чи­лось - ус­та­но­вить null
    try { mc.hig­h­lig­h­ted = new Bit­map(di2.Ful­lNa­me + "\" + tsa[6]); } // то же для под­с­ве­чен­но­го
    catch { mc.hig­h­lig­h­ted = null; }
    try { mc.pres­sed = new Bit­map(di2.Ful­lNa­me + "\" + tsa[7]); } // то же для на­жа­то­го
    catch { mc.pres­sed = null; }
    tsa2 = tsa[8].Split(',');
    mc.font = new Font(tsa2[0], flo­at.Par­se(tsa2[1]), (Fon­t­S­t­y­le)Int32.Par­se(tsa2[2])); // соз­дать шрифт по па­ра­мет­рам
    mc.Text = tsa[9]; // за­пи­сать текст
    skins[i].controls.Add(mc); // до­ба­вить объ­ект опи­са­ния в кол­лек­цию
    }
    }
    catch { }
    finally {
    sr.Close();
    }
    i++;
    }
    }
    
    и при­ме­не­ния:
    
    internal vo­id Ap­ply­S­kin1(int idx) {
    Controls.Clear(); // уб­рать все эле­мен­ты уп­рав­ле­ния с фор­мы
    TransparencyKey = skins[idx].tran­s­pa­ren­t­Key; // ус­та­но­вить цвет проз­рач­нос­ти
    BackColor = Tran­s­pa­ren­c­y­Key; // фон фор­мы = проз­рач­нос­ти
    Size = skins[idx].for­m­Si­ze; // ус­та­но­вить раз­мер фор­мы
    for (int i = 0; i ‹ skins[idx].con­t­rols.Co­unt; i++) { // для каж­до­го эле­мен­та уп­рав­ле­ния из кол­лек­ции
    if (((MyCon­t­rol)skins[idx].con­t­rols[i]).type == 0) { // ес­ли эле­мент - pic­tu­re­Box
    PictureBox p = new Pic­tu­re­Box(); // соз­дать
    p.Margin = new Pad­ding(0); // ус­та­но­вить по­ля = 0
    p.BorderStyle = Bor­der­S­t­y­le.No­ne; // бор­дю­ра нет
    p.Location = ((MyCon­t­rol)skins[idx].con­t­rols[i]).Lo­ca­ti­on; // ус­та­но­вить по­ло­же­ние
    p.Size = ((MyCon­t­rol)skins[idx].con­t­rols[i]).Si­ze; // раз­мер
    p.BackColor = ((MyCon­t­rol)skins[idx].con­t­rols[i]).bgCo­lor; // фон
    p.Image = ((MyCon­t­rol)skins[idx].con­t­rols[i]).nor­mal; // кар­тин­ку
    p.MouseDown += new Mo­use­Even­t­Han­d­ler(drag_mo­use­Down); // со­бы­тия пе­ре­тас­ки­ва­ния
    p.MouseUp += new Mo­use­Even­t­Han­d­ler(drag_mo­use­Up);
    p.MouseMove += new Mo­use­Even­t­Han­d­ler(drag_mo­use­Mo­ve);
    p.Tag = i; // ин­декс в кол­лек­ции
    Controls.Add(p); // до­ба­вить к фор­ме
    }
    else { // ес­ли кноп­ка (лю­бая из трех)
    Button b = new But­ton(); // соз­дать
    b.FlatStyle = Flat­S­t­y­le.Flat; // сде­лать плос­кой
    b.FlatAppearance.BorderSize = 0; // без бор­дю­ра
    b.FlatAppearance.MouseOverBackColor = skins[idx].tran­s­pa­ren­t­Key; // с проз­рач­ным бор­дю­ром
    b.FlatAppearance.MouseDownBackColor = skins[idx].tran­s­pa­ren­t­Key; // во всех сос­то­яни­ях
    b.Margin = new Pad­ding(0); // без по­лей
    b.Location = ((MyCon­t­rol)skins[idx].con­t­rols[i]).Lo­ca­ti­on; // рас­по­ло­жить
    b.Size = ((MyCon­t­rol)skins[idx].con­t­rols[i]).Si­ze; // за­дать раз­мер
    b.BackColor = ((MyCon­t­rol)skins[idx].con­t­rols[i]).bgCo­lor; // ус­та­но­вить цвет фо­на
    b.ForeColor = ((MyCon­t­rol)skins[idx].con­t­rols[i]).fgCo­lor; // цвет шриф­та
    b.Font = ((MyCon­t­rol)skins[idx].con­t­rols[i]).font; // шрифт
    b.TextAlign = Con­ten­tA­lig­n­ment.Mid­dle­Cen­ter; // вы­рав­ни­ва­ние тек­с­та
    b.Image = ((MyCon­t­rol)skins[idx].con­t­rols[i]).nor­mal; // кар­тин­ку
    if (((MyCon­t­rol)skins[idx].con­t­rols[i]).type == 1) { // ес­ли кноп­ка 1
    b.Click += new Even­t­Han­d­ler(but­ton1_Click); // ус­та­но­вить на­жа­тие = на­жа­тие кноп­ки 1
    }
    else if (((MyCon­t­rol)skins[idx].con­t­rols[i]).type == 2) { // ана­ло­гич­но кноп­ка 2
    b.Click += new Even­t­Han­d­ler(but­ton2_Click);
    }
    else if (((MyCon­t­rol)skins[idx].con­t­rols[i]).type == 3) { // ана­ло­гич­но кноп­ка 3
    b.Click += new Even­t­Han­d­ler(but­ton3_Click);
    }
    b.Text = ((MyCon­t­rol)skins[idx].con­t­rols[i]).Text; // ус­та­но­вить текст
    b.MouseDown += new Mo­use­Even­t­Han­d­ler(mo­use­Down); // со­бы­тия
    b.MouseUp += new Mo­use­Even­t­Han­d­ler(mo­use­Up); // сме­ны
    b.MouseEnter += new Even­t­Han­d­ler(mo­use­En­ter); // кар­ти­нок
    b.MouseLeave += new Even­t­Han­d­ler(mo­use­Le­ave);
    b.Tag = i; // ин­декс в кол­лек­ции
    Controls.Add(b); // до­ба­вить к фор­ме
    }
    }
    currentSkin = idx; // сме­нить ин­декс те­ку­щей шкур­ки
    }
    
    Добавим в кон­с­т­рук­тор вы­зов этих двух фун­к­ций­:
    
    public Form1() {
    InitializeComponent();
    LoadSkins1(); // заг­ру­зить шкур­ки
    ApplySkin1(0); // при­ме­нить пер­вую
    }
        Запускаем, про­ве­ря­ем:
    
    Рыжие кру­жоч­ки на­жи­ма­ют­ся и под­с­ве­чи­ва­ют­ся, за мор­ков­ные ли­нии мож­но пе­ре­тас­ки­вать.
    
    Девушка вся пе­ре­тас­ки­ва­ет­ся, кры­лыш­ки при­под­ни­ма­ют­ся при на­ве­де­нии. При на­жа­тии на кры­лыш­ки - прог­рам­ма вы­хо­дит.
    
    Можно бы­ло ко­неч­но до­ба­вить воз­мож­ность ус­та­нов­ки фо­но­во­го изоб­ра­же­ния для фор­мы, и не ма­ят­ся с pic­tu­re­Box, но с та­ким под­хо­дом воз­мож­нос­тей чуть боль­ше.
    Каталог про­ек­та для MS Vi­su­al Stu­dio 2005 с при­ме­ром на­хо­дит­ся в ар­хи­ве при­ме­ров, под­ка­та­лог skins1 для пер­во­го ме­то­да и skins2 для вто­ро­го.
    
6. Перехватчик всех ошибок.
    
    В сов­ре­мен­ных прог­рам­мах ста­ло мод­но де­лать окош­ки ко­то­рые нас­то­ятель­но ре­ко­мен­ду­ют от­п­ра­вить от­чет, пред­ла­га­ют сох­ра­нить ра­бо­ту и пе­ре­за­пус­тить прог­рам­му пос­ле зак­ры­тия. Та­кое ок­но по­яв­ля­ет­ся во всех при­ло­же­ни­ях Mic­ro­soft, сде­лан­ных пос­ле 2002 го­да и еще в очень мно­гих. Ес­ли вы хо­ти­те сде­лать что-то по­доб­ное, то это де­ла­ет­ся при­мер­но так:
    
    Сначала соз­да­дим фор­му, с од­ной кноп­кой - что­бы толь­ко ими­ти­ро­вать ошиб­ку:
    
    Теперь на кноп­ку са­жа­ем та­кой код:
    
    private vo­id but­ton1_Click(obj­ect sen­der, Even­tArgs e) {
    throw new Ar­gu­men­tEx­cep­ti­on("Неп­ра­виль­ное зна­че­ние па­ра­мет­ра", "index");
    }
    
    При на­жа­тии - ки­нуть ошиб­ку о том что па­ра­метр in­dex при­нял не­до­пус­ти­мое зна­че­ние.
    Теперь идем в файл Prog­ram.cs, ту­да, где фун­к­ция Ma­in, и пе­ре­пи­сы­ва­ем ее:
    
    static vo­id Ma­in() {
    Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); //отлав­ли­вать все ошиб­ки, вы­шед­шие из прог­рам­мы
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Form1 f1 = new Form1();
    Application.ThreadException += new System.Thre­ading.Thre­adEx­cep­ti­onE­ven­t­Han­d­ler(Appli­ca­ti­on_Thre­adEx­cep­ti­on);// за­дать об­ра­бот­чик оши­бок
    Application.Run(f1);
    }
    
    Теперь опи­шем фун­к­цию об­ра­бот­ки оши­бок. Бу­дем ис­хо­дить из то­го, что у нас бу­дет фор­ма для по­ка­за ин­фор­ма­ции об ошиб­ке:
    
    static vo­id Ap­pli­ca­ti­on_Thre­adEx­cep­ti­on(obj­ect sen­der, System.Thre­ading.Thre­adEx­cep­ti­onE­ven­tArgs e) {
    ApplicationCrashForm cf = new Ap­pli­ca­ti­on­C­ras­h­Form(); // фор­ма по­ка­за ин­фор­ма­ции об ошиб­ке
    cf.FillData(e.Exception); // за­пол­нить фор­му ин­фор­ма­ци­ей
    if (cf.Show­Di­alog() == Di­alog­Re­sult.OK) { // ес­ли на­жа­ли сох­ра­нить
    ((Form1)Application.OpenForms["Form1"]).SaveAs(); // сох­ра­нить ра­бо­ту
    }
    if (cf.chec­k­Box1.Chec­ked) Ap­pli­ca­ti­on.Res­tart(); // ес­ли га­лоч­ка пе­ре­за­пус­тить ус­та­нов­ле­на - пе­ре­за­пус­тить прог­рам­му
    else Ap­pli­ca­ti­on.Exit(); // ес­ли нет - прос­то вый­ти
    }
    
    Ну соб­с­т­вен­но, ос­та­лось толь­ко соз­дать фор­му по­ка­за оши­бок:
    
    Не за­будь­те пос­та­вить для chec­k­Box1 Mo­di­fi­ers=inter­nal.
    Теперь опи­шем код фор­мы:
    
    internal vo­id Fil­lDa­ta(Excep­ti­on ex) {
    StringBuilder sb = new Strin­g­Bu­il­der();
    FillExceptionInfo(ex, sb);
    textBox1.Text = sb.ToS­t­ring();
    textBox1.SelectionLength = 0;
    }
    private vo­id Fil­lEx­cep­ti­onIn­fo(Excep­ti­on ex, Strin­g­Bu­il­der sb) {
    sb.AppendFormat("Произошла ошиб­ка ти­па {0}\r\n", ex.Get­T­y­pe());
    sb.AppendFormat("Объект выз­вав­ший ошиб­ку: {0}\r\n", ex.So­ur­ce);
    sb.AppendFormat("Ошибка про­изош­ла в ме­то­де {0}\r\n", ex.Tar­get­Si­te);
    sb.AppendFormat("Основная ин­фор­ма­ция об ошиб­ке: {0}\r\n", ex.Mes­sa­ge);
    sb.AppendFormat("Стек вы­зо­ва: {0}\r\n\r\n", ex.Stac­k­T­ra­ce);
    if (ex.Inne­rEx­cep­ti­on != null) Fil­lEx­cep­ti­onIn­fo(ex.Inne­rEx­cep­ti­on, sb);
    }
    
    Думаю тут все по­нят­но.
    Последние штри­хи - фун­к­ции на­жа­тия на лин­ки в фор­ме ошиб­ки:
    
    private vo­id lin­k­La­bel1_Lin­k­C­lic­ked(obj­ect sen­der, Lin­k­La­bel­Lin­k­C­lic­ke­dE­ven­tArgs e) {
    Process.Start("http://www.domain.com/techsupport_forum.cgi");
    }
    private vo­id lin­k­La­bel2_Lin­k­C­lic­ked(obj­ect sen­der, Lin­k­La­bel­Lin­k­C­lic­ke­dE­ven­tArgs e) {
    StringBuilder sb = new Strin­g­Bu­il­der();
    sb.Append("mailto:techsupport@domain.com?subject=Program_Bugbody=");
    string ts = tex­t­Box1.Text.Rep­la­ce("\r\n", "‹br›");
    string amp = Uri.He­xEs­ca­pe('');
    ts = ts.Rep­la­ce("", amp);
    sb.Append(ts);
    Process.Start(sb.ToString());
    }
    
    И фун­к­цию сох­ра­не­ния ра­бо­ты Sa­ve­As() для Form1 не за­будь­те сде­лать.
    Вот и все.
    Каталог про­ек­та для MS Vi­su­al Stu­dio 2005 с при­ме­ром на­хо­дит­ся в ар­хи­ве при­ме­ров, под­ка­та­лог ex­cep­ti­on.
    
7. Самодельный бекапер.
    У ме­ня од­наж­ды на по­вес­т­ке дня встал воп­рос о прог­рам­ме-бе­ка­пе­ре, ко­то­рая бу­дет де­лать сле­ду­ющие ве­щи:
    1. Ко­пи­ро­вать все фай­лы из ука­зан­ных ка­та­ло­гов, чья да­та пос­лед­не­го из­ме­не­ния поз­д­нее за­дан­ной да­ты (дня, ког­да я пе­ре­сел на но­ут).
    2. Для уже ско­пи­ро­ван­ных фай­лов - ко­пи­ро­вать фай­лы, толь­ко ес­ли да­та из­ме­не­ния поз­д­нее, чем у ско­пи­ро­ван­ной ко­пии.
    3. Ка­та­лог для сох­ра­не­ния дол­ж­но быть мож­но выб­рать и в ло­каль­ной се­ти.
    
    В оче­ред­ной раз на­по­рол­ся: ра­бо­ты бы­ло мно­го и я ре­шил что быс­т­рее най­ду, чем на­пи­шу сам... по­ис­кал - на­шел с три де­сят­ка по­доб­ных прог­рамм. Поп­ро­бо­вал од­ну - не де­ла­ет все­го, что на­до, дру­гую - не под­дер­жи­ва­ет сеть, третью - хо­чет де­нег, хо­тя на сай­те ска­за­но "бес­п­лат­но"... ко­ро­че, по­те­ряв час, я ре­шил что быс­т­рее все-та­ки на­пи­сать са­мо­му.
    
    Итак, соз­да­ем при­мер­но та­кое ок­но.
    
    То, что вни­зу - это prog­res­sBar. До­пол­ня­ем ок­но bac­k­g­ro­un­d­Wor­ker и fol­der­B­row­ser­Ca­ta­log. Ус­та­но­вим prog­res­sBar.Vi­sib­le = fal­se;
    Теперь соз­да­дим нас­т­рой­ки прог­рам­мы. Нам на­до бу­дет пом­нить - ка­та­ло­ги, ко­то­рые на­до спа­сать; ка­та­лог, ку­да на­до спа­сать; да­ту, пос­ле ко­то­рой на­до спа­сать.
    Открываем Set­tings про­ек­та (Pro­j­ect Pro­per­ti­es -› Set­tings) и соз­да­ем та­кое вот:
    
    DirsToSave - это System.Col­lec­ti­ons.Spe­ci­ali­zed.Strin­g­Col­lec­ti­on
    SinceWhen - System.Da­te­Ti­me
    Значения пи­ши­те лю­бые, но дей­ст­ви­тель­ные и не ос­тав­ляй­те по­ля пус­ты­ми.
    
    Переходим к ко­ду прог­рам­мы. Для на­ча­ла соз­да­дим объ­ект set­tings:
    
    SynchroSaver.Properties.Settings sets;
    
    Теперь опи­шем код заг­руз­ки фор­мы - ини­ци­али­зи­ро­вать bac­k­g­ro­un­d­Wor­ker и заг­ру­зить нас­т­рой­ки.
    
    private vo­id Form1_Lo­ad(obj­ect sen­der, Even­tArgs e) { // заг­руз­ка фор­мы
    sets = Synchro­Sa­ver.Pro­per­ti­es.Set­tin­gs.De­fa­ult; // заг­ру­жа­ем нас­т­рой­ки
    for (int i = 0; i ‹ sets.Dir­s­To­Sa­ve.Co­unt; i++) { // для каж­до­го ка­та­ло­га на сох­ра­не­ние
    listBox1.Items.Add(sets.DirsToSave[i]); // до­ба­вить в lis­t­Box
    }
    textBox1.Text = sets.Bac­kup­Dir; // на­пи­сать ка­та­лог ку­да сох­ра­нять
    dateTimePicker1.Value = sets.Sin­ceW­hen; // ус­та­но­вить да­ту
    }
    
    Как след­с­т­вие - про­пи­шем и зак­ры­тие фор­мы:
    
    private vo­id Form1_For­m­C­lo­sing(obj­ect sen­der, For­m­C­lo­sin­gE­ven­tArgs e) {
    if (e.Clo­se­Re­ason == Clo­se­Re­ason.Win­dow­s­S­hut­Down) { // ес­ли Win­dows зак­ры­ва­ет­ся
    if (bac­k­g­ro­un­d­Wor­ker1.IsBusy) { // ес­ли прог­рам­ма ра­бо­та­ет
    backgroundWorker1.CancelAsync(); // пос­лать от­ме­ну
    }
    else { // ес­ли не ра­бо­та­ет
    sets.Save(); // сох­ра­нить те­ку­щие нас­т­рой­ки
    }
    }
    else { // ес­ли про­гу зак­ры­ва­ет не сис­те­ма
    if (bac­k­g­ro­un­d­Wor­ker1.IsBusy) { // ес­ли ра­бо­та­ет
    e.Cancel = true; // от­ме­нить зак­ры­тие
    return;
    }
    else { // ес­ли не ра­бо­та­ет
    sets.Save(); // сох­ра­нить нас­т­рой­ки
    }
    }
    }
    
    Теперь опи­шем кноп­ки:
    Добавить ка­та­лог:
    private vo­id but­ton1_Click(obj­ect sen­der, Even­tArgs e) {
    if (fol­der­B­row­ser­Di­alog1.Show­Di­alog() == Di­alog­Re­sult.OK) { // ес­ли ка­та­лог выб­ра­ли
    listBox1.Items.Add(folderBrowserDialog1.SelectedPath); // до­ба­вить в lis­t­Box
    }
    }
    
    Убрать ка­та­лог:
    private vo­id but­ton2_Click(obj­ect sen­der, Even­tArgs e) {
    if (lis­t­Box1.Se­lec­te­dIn­dex != -1) { // ес­ли ка­та­лог вы­де­лен
    listBox1.Items.RemoveAt(listBox1.SelectedIndex); // уб­рать его из спис­ка
    }
    }
    
    Обзор... для ка­та­ло­га сох­ра­не­ния:
    private vo­id but­ton3_Click(obj­ect sen­der, Even­tArgs e) {
    if (fol­der­B­row­ser­Di­alog1.Show­Di­alog() == Di­alog­Re­sult.OK) { // ес­ли ка­та­лог выб­ра­ли
    textBox1.Text = fol­der­B­row­ser­Di­alog1.Se­lec­ted­Path; // про­пи­сать в tex­t­Box
    }
    }
    
    Запуск:
    проверяем ввод, от­к­лю­ча­ем эле­мен­ты уп­рав­ле­ния, под­го­тав­ли­ва­ем к за­пус­ку и за­пус­ка­ем...
    Эта же кноп­ка бу­дет кноп­кой От­ме­на, ес­ли про­га уже ра­бо­та­ет
    private vo­id but­ton4_Click(obj­ect sen­der, Even­tArgs e) {
    if (!bac­k­g­ro­un­d­Wor­ker1.IsBusy) { // ес­ли про­га не ра­бо­та­ет
    if (lis­t­Box1.Items.Co­unt == 0) { // ес­ли ка­та­ло­гов нет
    return; // вер­нуть­ся
    }
    if (!Di­rec­tory.Exis­ts(tex­t­Box1.Text)) re­turn; // ес­ли ка­та­лог ку­да спа­сать не су­щес­т­ву­ет - вер­нуть­ся
    DisableControls(); // от­к­лю­чить эле­мен­ты уп­рав­ле­ния
    backgroundWorker1.DoWork += new Do­Wor­kE­ven­t­Han­d­ler(bac­k­g­ro­un­d­Wor­ker1_Do­Work); // наз­на­чить event на­ча­ла ра­бо­ты
    backgroundWorker1.ProgressChanged += new Prog­res­sChan­ge­dE­ven­t­Han­d­ler(bac­k­g­ro­un­d­Wor­ker1_Prog­res­sChan­ged); // наз­на­чить event из­ме­не­ния прог­рес­са
    backgroundWorker1.RunWorkerCompleted += new Run­Wor­ker­Com­p­le­te­dE­ven­t­Han­d­ler(bac­k­g­ro­un­d­Wor­ker1_Run­Wor­ker­Com­p­le­ted); // наз­на­чить event окон­ча­ния ра­бо­ты
    sets.SinceWhen = da­te­Ti­me­Pic­ker1.Va­lue; // из­ме­нить да­ту в нас­т­рой­ках на вве­ден­ную
    sets.BackupDir = tex­t­Box1.Text; // ка­та­лог ку­да спа­сать то­же
    sets.DirsToSave.Clear(); // очис­тить спи­сок ка­та­ло­гов
    for (int i = 0; i ‹ lis­t­Box1.Items.Co­unt; i++) { // пош­туч­но
    sets.DirsToSave.Add(listBox1.Items[i].ToString()); // до­ба­вить ка­та­ло­ги в нас­т­рой­ки
    }
    backgroundWorker1.RunWorkerAsync(); // за­пус­тить ра­бот­ни­ка
    }
    else { // ес­ли про­га ра­бо­та­ет
    backgroundWorker1.CancelAsync(); // пос­лать от­ме­ну
    }
    }
    
    Ну те­перь опи­шем за­дан­ные со­бы­тия для bac­k­g­ro­un­d­Wor­ker:
    Начать ра­бо­ту:
    void bac­k­g­ro­un­d­Wor­ker1_Do­Work(obj­ect sen­der, Do­Wor­kE­ven­tArgs e) {
    backgroundWorker1.ReportProgress(0); // ус­та­но­вить прог­ресс 0
    for (int i = 0; i ‹ sets.Dir­s­To­Sa­ve.Co­unt; i++) { // для каж­до­го ка­та­ло­га для сох­ра­не­ния
    BackupDir(sets.DirsToSave[i].ToString(), i * 100 / sets.Dir­s­To­Sa­ve.Co­unt, 100 / sets.Dir­s­To­Sa­ve.Co­unt); // спас­ти ка­та­лог
    if (bac­k­g­ro­un­d­Wor­ker1.Can­cel­la­ti­on­Pen­ding) { // ес­ли пос­ла­ли от­ме­ну
    break;
    }
    }
    backgroundWorker1.ReportProgress(100); // ус­та­но­вить прог­ресс 100
    }
    
    Изменился прог­ресс:
    void bac­k­g­ro­un­d­Wor­ker1_Prog­res­sChan­ged(obj­ect sen­der, Prog­res­sChan­ge­dE­ven­tArgs e) {
    progressBar1.Value = e.Prog­res­sPer­cen­ta­ge; // ус­та­но­вить зна­че­ние prog­res­sBar = прог­рес­су ра­бот­ни­ка
    }
    
    Работа за­вер­ше­на:
    void bac­k­g­ro­un­d­Wor­ker1_Run­Wor­ker­Com­p­le­ted(obj­ect sen­der, Run­Wor­ker­Com­p­le­te­dE­ven­tArgs e) {
    EnableControls(); // вклю­чить эле­мен­ты уп­рав­ле­ния
    }
    
    Отключение эле­мен­тов:
    отключаем все, кро­ме кноп­ки за­пуск, ко­то­рую ме­ня­ем на От­ме­ну
    private vo­id Di­sab­le­Con­t­rols() {
    button1.Enabled = fal­se;
    button2.Enabled = fal­se;
    button3.Enabled = fal­se;
    progressBar1.Visible = true;
    listBox1.Enabled = fal­se;
    textBox1.Enabled = fal­se;
    dateTimePicker1.Enabled = fal­se;
    button4.Text = "Can­cel";
    }
    
    Включение эле­мен­тов:
    private vo­id Enab­le­Con­t­rols() {
    button1.Enabled = true;
    button2.Enabled = true;
    button3.Enabled = true;
    progressBar1.Visible = fal­se;
    listBox1.Enabled = true;
    textBox1.Enabled = true;
    dateTimePicker1.Enabled = true;
    button4.Text = "RUN";
    }
    
    Теперь са­мое глав­ное:
    Бекап ка­та­ло­га:
    Поскольку сох­ра­не­ние ка­та­ло­гов тре­бу­ет ре­кур­сии, эта фун­к­ция бу­дет за­пус­кать­ся для ка­та­ло­гов всех уров­ней. Для это­го и соз­да­ны па­ра­ме­ры prog­r­S­tart - на­чаль­ный прог­ресс и progr - до­ля это­го ка­та­ло­га в об­щем прог­рес­се:
    private vo­id Bac­kup­Dir(string dir, flo­at prog­r­S­tart, flo­at progr) {
    DirectoryInfo mdi = new Di­rec­tor­yIn­fo(dir); // ин­фор­ма­ция о ка­та­ло­ге
    DirectoryInfo[] dis = mdi.Get­Di­rec­to­ri­es(); // вло­жен­ные пап­ки
    for (int i = 0; i ‹ dis.Length; i++) { // для каж­дой вло­жен­ной пап­ки
    BackupDir(dis[i].FullName, prog­r­S­tart + i * progr / dis.Length, progr / dis.Length); // спас­ти
    }
    FileInfo[] fis = mdi.Get­Fi­les(); // фай­лы в ка­та­ло­ге
    for (int i = 0; i ‹ fis.Length; i++) { // для каж­до­го фай­ла
    if (fis[i].Las­t­W­ri­te­Ti­me › sets.Sin­ceW­hen) { // ес­ли пос­лед­нее из­ме­не­ние поз­д­нее ука­зан­ной да­ты
    if (Ne­ed­To­Copy(fis[i].Ful­lNa­me)) { // срав­нить с бе­кап вер­си­ей и ес­ли на­до ко­пи­ро­вать
    CopyFile(fis[i].FullName); // ско­пи­ро­вать файл
    }
    }
    }
    backgroundWorker1.ReportProgress((int)(progrStart + progr)); // от­чи­тать­ся о прог­рес­се
    }
    
    Проверка на не­об­хо­ди­мость ко­пи­ро­ва­ния:
    private bo­ol Ne­ed­To­Copy(string fn) {
    string fnr = fn;
    for (int i = 0; i ‹ sets.Dir­s­To­Sa­ve.Co­unt; i++) { // для каж­до­го ка­та­ло­га на спа­се­ние
    if (fnr.Con­ta­ins(sets.Dir­s­To­Sa­ve[i].ToS­t­ring())) { // ес­ли путь фай­ла со­дер­жит имя ка­та­ло­га
    fnr = fnr.Rep­la­ce(sets.Dir­s­To­Sa­ve[i].ToS­t­ring(), sets.Bac­kup­Dir); // за­ме­нить ка­та­лог от­ку­да на ка­та­лог ку­да
    break; // прер­вать цикл
    }
    }
    if (Fi­le.Exists(fnr)) { // ес­ли файл су­щес­т­ву­ет, т.е. уже ско­пи­ро­ван
    FileInfo fi = new Fi­le­In­fo(fn); // ин­фор­ма­ция о ра­бо­чем фай­ле
    FileInfo fi2 = new Fi­le­In­fo(fnr); // ин­фор­ма­ция о спа­сен­ном фай­ле
    if (fi.Las­t­W­ri­te­Ti­me › fi2.Las­t­W­ri­te­Ti­me) { // ес­ли файл был из­ме­нен пос­ле спа­се­ния
    return true; // вер­нуть прав­ду
    }
    else { // ес­ли нет
    return fal­se; // вер­нуть ложь
    }
    }
    else re­turn true; // ес­ли файл не су­щес­т­ву­ет - вер­нуть прав­ду
    }
    
    Копирование:
    private vo­id Cop­y­Fi­le(string fn) {
    string fnr = fn;
    for (int i = 0; i ‹ sets.Dir­s­To­Sa­ve.Co­unt; i++) { // то же, что и пред. фун­к­ция
    if (fnr.Con­ta­ins(sets.Dir­s­To­Sa­ve[i].ToS­t­ring())) {
    fnr = fnr.Rep­la­ce(sets.Dir­s­To­Sa­ve[i].ToS­t­ring(), sets.Bac­kup­Dir);
    break;
    }
    }
    if (!Di­rec­tory.Exis­ts(Path.Get­Di­rec­tor­y­Na­me(fnr))) { // ес­ли ка­та­лог не су­щес­т­ву­ет
    Directory.CreateDirectory(Path.GetDirectoryName(fnr)); // соз­дать
    }
    File.Copy(fn, fnr, true); // ско­пи­ро­вать, с пе­ре­за­писью
    }
    
    Вот соб­с­т­вен­но и все. 
    Что сде­ла­но неп­ра­виль­но, с точ­ки зре­ния ком­мер­чес­ко­го про­дук­та - от­ме­на ра­бо­та­ет толь­ко ког­да прог­рам­ма пе­ре­хо­дит от од­но­го глав­но­го ка­та­ло­га к дру­го­му глав­но­му.
    Что неп­ра­виль­но, с точ­ки зре­ния "гра­мот­ной­" прог­рам­мы - име­на ка­та­ло­гов не пе­ре­да­ют­ся фун­к­ци­ям, а те прос­тым пе­ре­бо­ром на­хо­дят нуж­ный.
    Каталог про­ек­та для MS Vi­su­al Stu­dio 2005 с при­ме­ром на­хо­дит­ся в ар­хи­ве при­ме­ров, под­ка­та­лог Synchro­Sa­ver.