Bir TStringList türevi..

// 15 Mayıs 2009 // Delphi, Programlama

Bir istek üzerine , mevcut VCL sınıflarından birisine onun bünyesinde barındırmadığı bir event yazma ile ilgili bir makale yayınlayacağım.Düşündüm taşındım hangi nesneye ne eklesem diye, VCL’in üreticileri sağolsunlar pek çok şeyi düşünmüşler.Benimde aklıma TStringList sınıfına birkaç olay eklemek geldi. Zaten amaç genel olarak ne yapıldığını anlayabilmek ondan sonrasında siz değerli arkadaşlarım kendi isteklerinize göre istediğiniz sınıfı özelleştirirsiniz. Bu yazım da OnBeforeAdd, OnAfterAdd olaylarını eklemeyi düşündüm önce.Ama daha sonra aklıma, filtreleme özelliği de eklemek geldi.Nasıl yani filtreleme ? Biz istediğimiz kadar kötü kelime (badword) belirtebilseydik, ve TStringList nesnemize ekleme yaparken sınıfımız eklemek istediğimiz metnin içerisinde kötü bir kelime geçiyor ise bize bir event’le cevap verseydi , sonra bizde o eventte “Evet, kabul ediyorum” yada “hadi oradan..!” diyebilseydik ne güzel olurdu değil mi ? Eh o zaman demek ki birde OnBadwordFind diye bir olay daha oluşturmalıyız. Sınıfın anafikrini sizlerle paylaştığımıza göre, gelin TStringList sınıfı etrafında biraz dolanalım..Araştırma yapmadan olmuyor bu işler.. ;) Yine her zamanki gibi Delphi Help’i açtık ve yazdık TStringList diye..Metodlarına bakıyoruz. Evet bizim kullanmamız gereken metodu Add metodu..Gelin tanımına şöyle bir bakalım:

  function Add(const S: string): Integer; override;

Gayet anlaşılır bir tanım, zaten hemen hemen hepimiz bir şekilde bu sınıfı ve yeteneklerini tanıyoruz.Fonksiyonun görevi listeye , parametre olarak geçilen metinsel değeri eklemek. Ancak dikkat ederseniz tanımın sonunda override anahtar kelimesi var.Demek ki bu fonksiyon TStringList sınıfının üst sınıflarından birisinde virtual olarak tanımlanmış ve burada çalışma tarzının değiştirilmesine/genişletilmesine ihtiyaç duyulmuş.Ehh biz daha bir genişleteceğiz, demek ki biz de bu metodu ezeceğiz.(override) Sınıf tanımımız başlangıçta şu şekilde görünecek:

TMyStringList = class(TStringList)
    function Add(const S : String) : Integer; override;
  end;

  function TMYStringList.Add(const S : String) : Integer;
  begin
    Result := inherited Add(S);
  end;

Yukarıdaki kod ile biz şu anda TStringList sınıfına hiçbir ek özellik getiremedik ama neler döndüğünü ;) anlayabilmek adına bu sade halini de yazmak istedim. Add fonksiyonun tanımında override direktifini kullandık, çünkü miras aldığımız TStringList sınıfının bu metodunu özelleştirmek istiyoruz.Add fonksiyonun içerisinde ise Result := inherited Add(S); çağrısını yaptık. Bunu bir üst sınıfın yaptığı kodlamamaları tekrar yazmak istemediğimiz için yaptık.Zaten bizim amacımız neydi ?

  • 1- Listeye herhangi bir mesaj eklenmeden önce bir olay tetiklemek.
  • 2- Listeye herhangi bir mesaj eklendikten sonra bir olay tetiklemek.
  • 3- Listeye herhangi bir mesaj eklenmeden önce gelen mesajın içeriğini kontrol etmek,istemediğimiz kötü kelimeler içeriyor ise bir olay tetiklemek ve olay içerisinde kullanıcının müsaade edip etmediği opsiyonuna bağlı olarak ekleme işlemine devam etmek yada ekleme yapmamak.
  • Bu amaçlar yakinen incelendiğinde tüm kontrolün Add fonksiyonu içerisinde yapılacağı anlaşılabilir. Peki biz Delphi’de olmayan bir olay yönetisi yazacağız da bunu nasıl yapacağız ? Öncelikle isteklerimizin bir analizini yapmakta fayda var. Birincisi ekleme işlemi gerçekleşmeden hemen önce bir olay tetikleneceğine göre, bu olay yöneticisine eklenmek istenen metni parametre olarak geçmeyi sağlasak ve programcı isterse bunu değiştirebilse..Hımm mantıklı bir istek gibi görünüyor..İhtiyaç hasıl olabilir.

    Peki bir diğer olayımız ne zaman gerçekleşecekti ! Metin listeye eklendikten hemen sonra. Hımm o olay yöneticisine de eklenen metni parametre olarak geçsek. Belki programcı global biryerlerde listesine ne eklendi ne zaman eklendi bilgi tutmak istiyor ? Bu da mantıklı gibi..Ama buraya geçirilen metin parametresinin değiştirilebilir olmaması gerekiyor. Tamama şimdi bunları uygulamaya başlayalım. Ancak metod işaretçileri kavramına uzak iseniz tam bu aşamada sizden ricam Delphi, İlginçlikler Diyarı..! isimli makalemi okumanız.Yabancı değilseniz hadi o zaman konuya bir giriş yapalım , zaten yeterince fazla şey yazdık, biraz da kod görelim ;)

    TOnBeforeAdd = procedure(Sender : TObject; var sWillAdd : String) of object;
      TOnAfterAdd = procedure(Sender : TObject; const sAdded : String) of object;
      TOnBadwordFind = procedure(Sender : TObject; var Accept : Boolean) of object;
    
      TMyStringList = class(TStringList)
      private
        fOnBefore : TOnBeforeAdd;
        fOnAfter : TOnAfterAdd;
        fOnBadwordFind : TOnBadwordFind;
    
      public
        function Add(const S : String) : Integer; override;
    
        property OnBefore : TOnBeforeAdd read fOnBefore write fOnBefore;
        property OnAfter : TOnAfterAdd read fOnAfter write fOnAfter;
        property OnBadwordFind : TOnBadwordFind read fOnBadwordFind write fOnBadwordFind;
      end;
    
      function TMyStringList.Add(const S : String) : Integer;
      begin
        Result := inherited Add(S);
      end;
    

    Koda bu aşamada 3 adet olay yöneticisini işaret eden özellik(property) ekledim.Ama kod hala birşey yapmıyor.Bizim bu olayları yönetecek aynı tipte procedure tanımlarına ihtiyacımız var.Onun için o tanımları da ekleyip içlerini dolduralım.

    TOnBeforeAdd = procedure(Sender : TObject; var sWillAdd : String) of object;
      TOnAfterAdd = procedure(Sender : TObject; const sAdded : String) of object;
      TOnBadwordFind = procedure(Sender : TObject; var Accept : Boolean) of object;
    
      TMyStringList = class(TStringList)
      private
        fOnBefore : TOnBeforeAdd;
        fOnAfter : TOnAfterAdd;
        fOnBadwordFind : TOnBadwordFind;
    
        fStr : String;
        fAccept : Boolean;
    
      protected
        procedure DoOnBefore(Sender : TObject; var sWillAdd : String); dynamic;
        procedure DoOnAfter(Sender : TObject; const sAdded : String); dynamic;
        procedure DoOnBadwordFind(Sender : TObject; var Accept : Boolean); dynamic;
    
      public
        function Add(const S : String) : Integer; override;
    
        property OnBefore : TOnBeforeAdd read fOnBefore write fOnBefore;
        property OnAfter : TOnAfterAdd read fOnAfter write fOnAfter;
        property OnBadwordFind : TOnBadwordFind read fOnBadwordFind write fOnBadwordFind;
      end;
    
      function TMyStringList.Add(const S : String) : Integer;
      begin
        Result := inherited Add(S);
      end;
    
      procedure TMyStringList.DoOnAfter(Sender: TObject; const sAdded: String);
      begin
        if Assigned(fOnAfter) then fOnAfter(Self, sAdded);
      end;
    
      procedure TMyStringList.DoOnBadwordFind(Sender: TObject; var Accept: Boolean);
      begin
        if Assigned(fOnBadwordFind) then
        begin
          fOnBadwordFind(Self, Accept);
          fAccept := Accept;
        end;
      end;
    
      procedure TMyStringList.DoOnBefore(Sender: TObject; var sWillAdd: String);
      begin
        if Assigned(fOnBefore) then
        begin
          fOnBefore(Self, sWillAdd);
          fStr := sWillAdd;
        end;
      end;
    

    Yukarıda iki adet değişkenin fStr ve fAccept değişkenlerinin yeni tanımımıza eklendiğini görüyoruz.Bunun haricinde asıl önemli olan Do.. metodlarının içlerinde neler olduğu.Bunu anlatabilmek aslında çok uzun bir konu ancak ben mümkün olduğunca kısa anlatmaya çalışacağım.Pointerlara özellikle de metod pointerlara biraz yakınlaşmak gerekiyor hakkı ile anlayabilmek için. Ancak olayın aslı şu; Delphi programlama esnasında form üzerine herhangi bir nesneyi bıraktığınızda o nesneye ait pek çok özelliğin ve pek çok olayın varolduğunu görmüşsünüzdür.Olaylar(events) sekmesine geçip herhangi bir olay’ın üzerinde çift tıkladığınızda yada Ctrl+Enter tuş kombinasyonuna bastığınızda Delphi sizin için içinde kod yazabileceğiniz bir metod tanımlar.Hımm buraya kadar herşey hepimizin rutin kullandığı şeyler ve herbirimiz bunları biliyoruz.Peki neden anlatıyorum..Aslında işin altında çok daha karmaşık mekanizmalar yatıyor, onlara değinebilmek adına bunları anlatıyorum. Diyelim ki formumuzun üzerine bir adet button attık , button’umuzu seçtik ve Object Inspector’dan OnClick olay yöneticisinde iken çift tıkladık, Delphi’de bize bir kod bloğu sundu.İçine de gönlümüze göre birşeyler yazdık.Ve deh(F9) dedik ve çalştırdık.Bastık Button’a yazdığımız kodlar çalışıyor.Ne kadar doğal bir sonuç, elbette hiçbirimiz bunun aksi bir sonuç beklemiyorduk ki zaten..Ama size sorarım, Delphi biz F9′a bastıktan hemen sonra hafızada rezervasyon yaptığı Button nesnesinin OnClick özelliğinde bizim kod yazdığımızı nasıl anlıyor ?? Evet geldik en önemli noktaya.. Delphi bunu pointerlar sayesinde anlıyor, nam-ı diğer metod pointer.

    Metod Pointer denilen şey aslında bir metodu işaret eden normal bir pointerdan başka birşey değil.Ve OnClick’de button sınıfında tanımlanmış bir özellik(property). Biz Object Inspector’da çift tıkladığımızda OnClick adlı property’nin gösterdiği değişken artık sizin yazdığınız kod bloğunun pointer adresini tutuyor. Delphi Button’a bir tıklama oluştuğunda ilgili koda dallanıyor ve o pointer nil’den farklı bir değere sahip ise o pointer’ın gösterdiği adresteki metodu çalıştırıyor(Bizlerin kod editöründe yazdığı satırlar). Şimdi gelin daha somut olarak anlatalım..Bu örnek üzerinde biraz konuşalım. Öncelikle TOnBeforeAdd isimli bir tip tanımlamışız ve bu tipten olan fOnBefore isimli değişkeni private bloğunda tanımlamışız.Ardından bu değişken ile çalışabilmek adına bir özellik(property) tanımlamışız ve adına OnBeforeAdd demişiz. Haa OnBeforeAdd ha OnButtonClick.. Tıpkı yukarıda anlattığım gibi biz OnBeforeAdd olayı üzerinde çift tıklarsak Delphi bize aşağıdaki gibi bir kod bloğu açacaktır:

    procedure T…BeforeAdd(Sender : TObject; var sWillAdd : String);
    begin
    
    end;
    

    Ardından da içine birşeyler yazıp, deh(F9) dediğimizde fOnBefore adlı değişkenimizin içine yukarıdaki procedure’nin adresi atılacaktır.Bu kısmı anlamanız gerçekten önemli.Kodumuzu incelemeye devam edelim.DoOnBefore, DoOnAfter ve DoOnBadwordFind isimli metodlar görüyoruz ve bu metodlar sizinde farkettiğiniz gibi TOnBeforeAdd, TOnAfterAdd ve TOnBadwordFind tanımlamaları ile aynı yapıda. Peki bunların içlerinde neler var ? Mesela DoOnAfter‘a bakalım:

    procedure TMyStringList.DoOnAfter(Sender: TObject; const sAdded: String);
    begin
      if Assigned(fOnAfter) then fOnAfter(Self, sAdded);
    end;
    

    Hatırlayacağınız üzere fOnAfter TOnAfter tipinde bir değişkendi..Ve Assigned fonksiyonu bir değişkenin değerinin nil olup olmadığını kontrol eder.Nil değil ise true Nil ise false döndürür.Yani Assigned yerine if fOnAfter <> nil de yazabilirdik. fOnAfter değişkeninin nil’den farklı olmasının tek koşulu sizin OnAfter isimli property’niz üzerinde çift tıklayarak yada Delphi kod ortamından bu olaya bir procedure bağlamanızdır. Eğer siz bunu yaptı iseniz fOnAfter isimli değişken nil’den farklı olacaktır.Peki hangi adresi tutuyor olacaktır ? Elbette ki sizin kod editöründe yazdığınız kod bloğunun (procedure’nin) adresini tutuyor olacaktır. if Assigned kıyaslamasından hemen sonraki çağrı aslında tam anlamıyla sizin yazdığınız kod bloğuna dallanılmasını sağlayacaktır. Umarım anlaşılmıştır, gerçi çok detay bir konu mümkün olduğunca sade anlatmaya çalıştım. Galiba biz birşeyi unuttuk buraya gelene kadar.O da kötü kelimeleri tutacak değişken.! Ben kötü kelimeleri bir TStringList sınıfında tutmak isteyeceğim, siz kendi kullanımlarınızda başka yöntemler kullanabilirsiniz elbette. Bunun için sınıfımıza bir private değişken bir constructor ve bir destructor ve bir property eklemek zorundayım.Kodumuz şu hale dönüşecek:

    TOnBeforeAdd = procedure(Sender : TObject; var sWillAdd : String) of object;
      TOnAfterAdd = procedure(Sender : TObject; const sAdded : String) of object;
      TOnBadwordFind = procedure(Sender : TObject; var Accept : Boolean) of object;
    
      TMyStringList = class(TStringList)
      private
        fOnBefore : TOnBeforeAdd;
        fOnAfter : TOnAfterAdd;
        fOnBadwordFind : TOnBadwordFind;
    
        fStr : String;
        fAccept : Boolean;
    
        fBadwords : TStrings;
      protected
        procedure DoOnBefore(Sender : TObject; var sWillAdd : String); dynamic;
        procedure DoOnAfter(Sender : TObject; const sAdded : String); dynamic;
        procedure DoOnBadwordFind(Sender : TObject; var Accept : Boolean); dynamic;
    
      public
        function Add(const S : String) : Integer; override;
    
        constructor Create;
        destructor Destroy; override;
        property BadWords : TStrings read fBadword write fBadwords;
    
        property OnBefore : TOnBeforeAdd read fOnBefore write fOnBefore;
        property OnAfter : TOnAfterAdd read fOnAfter write fOnAfter;
        property OnBadwordFind : TOnBadwordFind read fOnBadwordFind write fOnBadwordFind;
      end;
    
      constructor TMyStringList.Create;
      begin
        inherited;
        fAccept := false; // Varsayılan olarak kötü kelimelere izin verme.
    
        fBadwords := TStringList.Create;
    
        fBadwords.Add(’sex’);
        fBadwords.Add(‘adult’);
        fBadwords.Add(‘naked’);
        fBadwords.Add(‘çıplak’); // vs. vs..
      end;
    
      destructor Destroy;
      begin
        fBadwords.Free;
        inherited;
      end;
    
      function TMyStringList.Add(const S : String) : Integer;
      begin
        Result := inherited Add(S);
      end;
    
      procedure TMyStringList.DoOnAfter(Sender: TObject; const sAdded: String);
      begin
        if Assigned(fOnAfter) then fOnAfter(Self, sAdded);
      end;
    
      procedure TMyStringList.DoOnBadwordFind(Sender: TObject; var Accept: Boolean);
      begin
        if Assigned(fOnBadwordFind) then
        begin
          fOnBadwordFind(Self, Accept);
          fAccept := Accept;
        end;
      end;
    
      procedure TMyStringList.DoOnBefore(Sender: TObject; var sWillAdd: String);
      begin
        if Assigned(fOnBefore) then
        begin
          fOnBefore(Self, sWillAdd);
          fStr := sWillAdd;
        end;
      end;
    

    Yukarıda görüldüğü gibi sınıfımızın oluşturulması esnasında yeni bir kötü kelime tutan TStringList oluşturuyor ve içerisine kötü kelimelerimizi giriyoruz.Oluşturduğumuz bu fBadwords nesnemizi silebilmek için ise destructor içerisinde Free çağrısını yapıyoruz.Makalemizin başları sayılabilecek bir noktada en önemli kodların Add fonksiyonun da yazılacağını söylemiştik ama hala Add fonksiyonu üzerinde birşey yapamadık.Hadi artık en can alıcı kodlarımızı yazalım. Ben de yoruldum walla :)

    function TMyStringList.ContainsBadword(const sValue: String): Boolean;
      var
        iCounter : Integer;
      begin
        Result := false;
    
        for iCounter := 0 to Badwords.Count - 1 do
          if Pos(Badwords[iCounter], sValue)  0 then
          begin
            Result := true;
            Break;
          end;
      end;
    
      function TMyStringList.Add(const S: string): Integer;
      begin
        fStr := S;
        DoOnBefore(Self, fStr);
    
        if ContainsBadword(fStr) then
        begin
          DoOnBadwordFind(Self, fAccept);
          if not fAccept then Exit;
        end;
    
        Result := inherited Add(fStr);
        DoOnAfter(Self, fStr);
      end;
    

    Yukarıdaki kodumuzda öncelikle eklenmek istenen mesajı geçici değişkenimiz içerisinde depoluyoruz.Ardından DoOnBefore metodunu çağırıyoruz.Bu çağrım, eğer kullanıcı OnBefore olay yöneticisine herhangi bir kod yazmışsa o kod bloğuna dallanılması anlamını taşır.Tam bu aşamada bu olay yöneticisinin içerisinde eklenmek istenen mesajı değiştirebilir, yada sağına soluna yanına vs. birşeyler eklemiş olabiliriz.OnBeforeAdd olay yöneticisinin çağırılmasından hemen sonra yeni geri gelen mesaj bilgisinin kötü kelime içerip içermediğini kontrol ediyoruz.(ContainsBadword). Eğer içeriyor ise ve OnBadwordFind olay yöneticisine birşeyler yazdı isek o kod bloğuna dallanılıyor. Kullanıcının Accept parametresine false geçmesi ile biz bu mesajın bu listeye eklenemeyeceğine hükmediyoruz ve Exit ile Add fonksiyonundan dışarı çıkıyoruz.Eğer kötü kelime bulunamamış ise yada kullanıcı Accept ile true göndermiş ise o zaman ekleme işlemimizi üst sınıfın (TStringList) Add metodunu çağırarak tamamlıyoruz.Mesajımız eklendikten sonra ise OnAfter olay yöneticisine herhangi bir kod bloğu yazmıi isek o kod bloğunun çalışmasını sağlıyoruz. İşte size tüm kodlar:

    unit untNewStringList;
    
      interface
    
      uses
        Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,Forms,
        Dialogs, StdCtrls;
    
      type
        TForm1 = class(TForm)
          ListBox1: TListBox;
          Button1: TButton;
          procedure Button1Click(Sender: TObject);
        private
          { Private declarations }
        public
          { Public declarations }
    
          procedure Before(Sender : TObject; var sWillAdd : String);
          procedure After(Sender : TObject; const sAdded : String);
          procedure Find(Sender : TObject; var Accept : Boolean);
        end;
    
        TOnBeforeAdd = procedure(Sender : TObject; var sWillAdd : String) of object;
        TOnAfterAdd = procedure(Sender : TObject; const sAdded : String) of object;
        TOnBadwordFind = procedure(Sender : TObject; var Accept : Boolean) of object;
    
        TMyStringList = class(TStringList)
        private
          fOnBefore : TOnBeforeAdd;
          fOnAfter : TOnAfterAdd;
          fOnBadwordFind : TOnBadwordFind;
    
          fBadwords : TStrings;
          fStr : String;
          fAccept : Boolean;
    
          function ContainsBadword(const sValue : String) : Boolean;
    
          procedure DoOnBefore(Sender : TObject; var sWillAdd : String); dynamic;
          procedure DoOnAfter(Sender : TObject; const sAdded : String); dynamic;
          procedure DoOnBadwordFind(Sender : TObject; var Accept : Boolean); dynamic;
        public
          constructor Create;
          destructor Destroy; override;
    
          function Add(const S: string): Integer; override;
    
          property Badwords : TStrings read fBadwords write fBadwords;
          property OnBefore : TOnBeforeAdd read fOnBefore write fOnBefore;
          property OnAfter : TOnAfterAdd read fOnAfter write fOnAfter;
          property OnBadwordFind : TOnBadwordFind read fOnBadwordFind write fOnBadwordFind;
        end;
    
      var
        Form1: TForm1;
    
      implementation
    
      {$R *.dfm}
    
      procedure TForm1.After(Sender: TObject; const sAdded: String);
      begin
        ShowMessage(sAdded + ‘ eklendi !’);
      end;
    
      procedure TForm1.Before(Sender: TObject; var sWillAdd: String);
      begin
        sWillAdd := sWillAdd + ‘ birde bunu ekle be, işin ne ?’; // Buraya kötü kelime de ekleyebilirsiniz ;) 
      end;
    
      procedure TForm1.Button1Click(Sender: TObject);
      var
        s : TMyStringList;
      begin
        s := TMyStringList.Create;
        s.OnBefore := Before;
        s.OnAfter := After;
        s.OnBadwordFind := Find;
    
        s.Add(‘Tugrul’);
        s.Add(‘Bir mesaj’);
        s.Add(‘Normal içerik’);
        s.Add(’sex’);
    
        ListBox1.Items.Assign(s);
        FreeAndNil(s);
      end;
    
      { TMyStringList }
    
      function TMyStringList.Add(const S: string): Integer;
      begin
        fStr := S;
        DoOnBefore(Self, fStr);
    
        if ContainsBadword(fStr) then
        begin
          DoOnBadwordFind(Self, fAccept);
          if not fAccept then Exit;
        end;
    
        Result := inherited Add(fStr);
        DoOnAfter(Self, fStr);
      end;
    
      function TMyStringList.ContainsBadword(const sValue: String): Boolean;
      var
        iCounter : Integer;
      begin
        Result := false;
    
        for iCounter := 0 to Badwords.Count - 1 do
          if Pos(Badwords[iCounter], sValue)  0 then
          begin
            Result := true;
            Break;
          end;
      end;
    
      constructor TMyStringList.Create;
      begin
        inherited;
        fAccept := false;
    
        fBadwords := TStringList.Create;
    
        fBadwords.Add(’sex’);
        fBadwords.Add(‘adult’);
        fBadwords.Add(‘naked’);
        fBadwords.Add(‘çıplak’); // vs. vs..
      end;
    
      destructor TMyStringList.Destroy;
      begin
        fBadwords.Free;
        inherited;
      end;
    
      procedure TMyStringList.DoOnAfter(Sender: TObject; const sAdded: String);
      begin
        if Assigned(fOnAfter) then fOnAfter(Self, sAdded);
      end;
    
      procedure TMyStringList.DoOnBadwordFind(Sender: TObject;  var Accept: Boolean);
      begin
        if Assigned(fOnBadwordFind) then
        begin
          fOnBadwordFind(Self, Accept);
          fAccept := Accept;
        end;
      end;
    
      procedure TMyStringList.DoOnBefore(Sender: TObject; var sWillAdd: String);
      begin
        if Assigned(fOnBefore) then
        begin
          fOnBefore(Self, sWillAdd);
          fStr := sWillAdd;
        end;
      end;
    
      procedure TForm1.Find(Sender: TObject; var Accept: Boolean);
      begin
        Accept := true;
      end;
    
      end.
    

    Umarım sizleri bu uzun makalem ile sıkmamışımdır, konunun özüne hakim olan method pointer‘ın ne demek olduğunu bilen programcı arkadaşlarımdan bunca detaya girdiğim için özür diliyorum. ;)

    Saygılar, sevgiler..

    “Bir TStringList türevi..” için 6 Yorum

    1. geyikben diyor ki:

      Sıkmamı? Böyle bir anlatımı alabileceğin en pahalı kitaplarda bile bulamazsın hatta bu bilgiye hiçbir yerde ulaşamazsın. Mükemmeldi Elinize Sağlık. Teşekkür ederim. Soluksuz okudum

    2. Tuğrul HELVACI diyor ki:

      Şevk veren yorumunuza teşekkür ederim.

    3. Hasan MANZAK diyor ki:

      Bir süredir yazılarınızı takip etmiyordum. Fırsat bulduğum ilk anda, bu gece, şöyle bir girip bakayım dedim ve tek solukta okuduğum ve ‘gene’ bana yep yeni fikirler veren bu makalenizle karşılaştım :) Öncelikle tebrik ediyorum, hemen ardından da teşekkür ediyorum.

      Ellerinize sağlı; kolay gelsin.

    4. Hasan MANZAK diyor ki:

      Ve gördüm ki, 1 sene önce eklenmiş bir makaleyi ben yeni fark etmişim…

    5. Teşekkürler yararlı bilgi

    Yorum Yazın