創作內容

3 GP

[C#]動態多組一對一路徑單向的同步備份<局部程式>

作者:碎銀子│2017-09-02 14:40:31│巴幣:24│人氣:1084
因為在公司的內部網路常常LAG,所以同事習慣進行中的專案資料都是放在自己的PC,偏偏別的單位想看相關資料時不是當事人已下班就請假(絕對不是我!絕對不是我!),所以想說寫個程式自動將大家各自進行中的資料同步到Server上各自的資料夾下
,方便其他人查閱!

在此只針對核心的物件做解說,在C#監視檔案或資料夾是透過FileSystemEventHandler物件來達成,但是目標的變更常常會造成重複觸發,例如將C檔案從A資料夾搬移到被監視的B資料夾下,對OS而言是Created ->Changed->Changed->Changed
一共四次的觸發,因此為了避免程式的重複動作必須建立比對的機制,像我就是透過比對檔案的存取時間以及檔案大小來判斷.

此外在下方程式裡可以看到當監視被觸發後,我對其檔案進行操作時都是透過新的執行緒,其原因是當觸發發生時亦代表OS正在操作此資料,因此我們並無法對其控制,所以我建立新的執行緒等待OS操作完畢後快速介入操作

完整程式碼

  1. using System;  
  2. using System.IO;  
  3. using System.Runtime.InteropServices;  
  4. using System.Threading;  
  5.   
  6. namespace FileSynchronize  
  7. {  
  8.     public class FileFolderWatcher  
  9.     {  
  10.         private String ClassNickName = "FileFolderWatcher";  
  11.         private DirectoryInfo WatcherPath, BackupPath;  
  12.         private DirectoryInfo dirInfo;  
  13.         private FileSystemWatcher _watch = new FileSystemWatcher();  
  14.         private Comparison comparison;  
  15.         private LogFileManagement LFM = Form_FileSynchronize.LFM;  
  16.         private String FilterType = "*.*";  
  17.   
  18.   
  19.         public FileFolderWatcher(DirectoryInfo Watcher_Path, DirectoryInfo Backup_Path)  
  20.         {  
  21.             WatcherPath = Watcher_Path;  
  22.             BackupPath = Backup_Path;  
  23.             comparison = new Comparison();  
  24.             FilterType = "*.*";  
  25.             FileWatcher();  
  26.         }  
  27.         public FileFolderWatcher(String NickName, DirectoryInfo Watcher_Path, DirectoryInfo Backup_Path)  
  28.         {  
  29.             ClassNickName = NickName;  
  30.             WatcherPath = Watcher_Path;  
  31.             BackupPath = Backup_Path;  
  32.             comparison = new Comparison();  
  33.             FilterType = "*.*";  
  34.             FileWatcher();            
  35.         }  
  36.         public FileFolderWatcher(String NickName, DirectoryInfo Watcher_Path, DirectoryInfo Backup_Path,String Filter)  
  37.         {  
  38.             ClassNickName = NickName;  
  39.             WatcherPath = Watcher_Path;  
  40.             BackupPath = Backup_Path;  
  41.             comparison = new Comparison();  
  42.             FilterType = Filter;  
  43.             FileWatcher();  
  44.         }  
  45.         private void FileWatcher()  
  46.         {  
  47.   
  48.             //設定所要監控的資料夾  
  49.             //MessageBox.Show(WatcherPath.FullName);  
  50.             _watch.Path = @WatcherPath.FullName;  
  51.   
  52.             //設定所要監控的變更類型  
  53.             //NotifyFilters.LastAccess      監視開啟時間  
  54.             //NotifyFilters.LastWrite       監視寫入時間  
  55.             //NotifyFilters.Size            監視檔案大小  
  56.             //NotifyFilters.FileName        監視檔名變更  
  57.             //NotifyFilters.DirectoryName   監視目錄變更  
  58.             _watch.NotifyFilter = NotifyFilters.Size | NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;  
  59.   
  60.             //設定所要監控的檔案 "*.txt|*.doc|*.jpg"  
  61.             _watch.Filter = FilterType;  
  62.   
  63.             //設定是否監控子資料夾  
  64.             _watch.IncludeSubdirectories = true;  
  65.   
  66.             //設定觸發事件  
  67.             _watch.Created += new FileSystemEventHandler(_watch_FileSystemEventHandler);  
  68.             _watch.Changed += new FileSystemEventHandler(_watch_FileSystemEventHandler);  
  69.             _watch.Renamed += new RenamedEventHandler(_watch_Renamed);  
  70.             _watch.Deleted += new FileSystemEventHandler(_watch_FileSystemEventHandler);  
  71.   
  72.       
  73.            // run();  //啟動元件  
  74.         }  
  75.   
  76.         private void _watch_FileSystemEventHandler(object sender, FileSystemEventArgs e)  
  77.         {  
  78.              
  79.             dirInfo = new DirectoryInfo(e.FullPath.ToString());  
  80.             // MessageBox.Show(e.ChangeType.ToString());  
  81.             if (!comparison.DynamicFolderFile(dirInfo))     //確定與先前是否相同  
  82.             {  
  83.                 comparison.setFileSystemInfo(dirInfo);    //設為新的比對參考  
  84.   
  85.                 switch (e.ChangeType.ToString())  
  86.                 {  
  87.                     case "Created":              
  88.                         _watch_Created(sender, e);  
  89.                          
  90.                         break;  
  91.                     case "Changed":  
  92.                           _watch_Changed(sender, e);  
  93.                           
  94.                         break;  
  95.                     case "Deleted":  
  96.                           _watch_Deleted(sender, e);  
  97.               
  98.                         break;  
  99.                     default:                   
  100.                         //MessageBox.Show("default");  
  101.                         break;  
  102.                 }  
  103.   
  104.             }  
  105.         }  
  106.   
  107.   
  108.         // <summary>  
  109.         // 當所監控的資料夾有建檔案時觸發  
  110.         // </summary>  
  111.         private void _watch_Created(object sender, FileSystemEventArgs e)  
  112.         {  
  113.                  
  114.                 FileInfo fa = new FileInfo(e.FullPath);  
  115.                 string difference = comparison.ExcludeTitle(fa, WatcherPath);  
  116.                 FileInfo fb = new FileInfo(BackupPath.FullName + difference);  
  117.                 DynamicFileSystemInfoControl DFIC = new DynamicFileSystemInfoControl(fa, fb, true);  
  118.                 DFIC.ThreadName(ClassNickName);  
  119.                 ThreadPool.QueueUserWorkItem(DFIC.Created,null);  
  120.         }  
  121.    
  122.   
  123.         // <summary>  
  124.         // 當所監控的資料夾檔案內容有異動時觸發  
  125.         // </summary>  
  126.         private void _watch_Changed(object sender, FileSystemEventArgs e)  
  127.         {  
  128.             FileInfo fa = new FileInfo(e.FullPath);  
  129.             string difference = comparison.ExcludeTitle(fa, WatcherPath);  
  130.             FileInfo fb = new FileInfo(BackupPath.FullName + difference);  
  131.             DynamicFileSystemInfoControl DFIC = new DynamicFileSystemInfoControl(fa, fb, true);  
  132.             DFIC.ThreadName(ClassNickName);  
  133.             ThreadPool.QueueUserWorkItem(DFIC.Changed, null);  
  134.         }  
  135.   
  136.         // <summary>  
  137.         // 當所監控的資料夾有檔案重新命名時觸發  
  138.         // </summary>  
  139.         private void _watch_Renamed(object sender, RenamedEventArgs e)  
  140.         {  
  141.   
  142.             FileInfo buffer = new FileInfo(e.OldFullPath);  
  143.             string difference = comparison.ExcludeTitle(buffer, WatcherPath);  
  144.             FileInfo oldName = new FileInfo(BackupPath.FullName + difference);  
  145.             buffer = new FileInfo(e.FullPath);  
  146.             difference = comparison.ExcludeTitle(buffer, WatcherPath);  
  147.             FileInfo newName = new FileInfo(BackupPath.FullName + difference);  
  148.             DynamicFileSystemInfoControl DFIC=null;  
  149.             if (oldName.Exists)  //確認BackupPath裡此檔案是否存在  
  150.             {//存在則rename   
  151.                 DFIC = new DynamicFileSystemInfoControl(oldName, newName, true);  
  152.                 DFIC.ThreadName(ClassNickName);  
  153.                 //Thread workerThread = new Thread(DFIC.Renamed);  
  154.                 //workerThread.Start();  
  155.                 ThreadPool.QueueUserWorkItem(DFIC.Renamed, null);  
  156.             }  
  157.             else  
  158.             {  
  159.                 //不存在則從WatcherPath複製檔案  
  160.                 DFIC = new DynamicFileSystemInfoControl(buffer, newName, true);  
  161.                 DFIC.ThreadName(ClassNickName);  
  162.                 ThreadPool.QueueUserWorkItem(DFIC.CopyTo, null);  
  163.             }  
  164.         }  
  165.   
  166.         // <summary>  
  167.         // 當所監控的資料夾檔案被刪除時觸發  
  168.         // </summary>  
  169.         private void _watch_Deleted(object sender, FileSystemEventArgs e)  
  170.         {  
  171.             FileInfo fa = new FileInfo(e.FullPath);  
  172.             string difference = comparison.ExcludeTitle(fa, WatcherPath);  
  173.             FileInfo fb = new FileInfo(BackupPath.FullName + difference);          
  174.             DynamicFileSystemInfoControl DFIC = new DynamicFileSystemInfoControl(fa, fb, true);  
  175.             DFIC.ThreadName(ClassNickName);  
  176.             //Thread workerThread = new Thread(DFIC.Deleted);  
  177.             //workerThread.Start();  
  178.             ThreadPool.QueueUserWorkItem(DFIC.Deleted, null);  
  179.         }  
  180.   
  181.         public void closed()  
  182.         {  
  183.             _watch.EnableRaisingEvents = false;  
  184.   
  185.             String TXT = "";  
  186.             TXT += "[" + ClassNickName + "]"; //寫入監視名稱  
  187.             TXT += "[Watcher Closed]";//操作行為  
  188.             LFM.writeLog(TXT);  
  189.         }  
  190.         public void closed(String messge)  
  191.         {  
  192.             _watch.EnableRaisingEvents = false;  
  193.   
  194.             String TXT = "";  
  195.             TXT += "[" + ClassNickName + "]"; //寫入監視名稱  
  196.            // TXT += "[Watcher Closed]";//操作行為  
  197.             TXT += messge;//操作行為  
  198.             LFM.writeLog(TXT);  
  199.         }  
  200.         public bool isRun()  
  201.         {  
  202.             return _watch.EnableRaisingEvents;  
  203.         }  
  204.         public void run()  
  205.         {  
  206.             if (WatcherPath == null || BackupPath == null) { closed("[Watcher Run][Error][Path is Null]"); return; }  //路徑錯誤關閉監視並離開  
  207.             String TXT = "";  
  208.             if (!WatcherPath.Exists) {  
  209.                 TXT = "";  
  210.                 TXT += "[" + ClassNickName + "]"; //寫入監視名稱  
  211.                 TXT += "[Watcher Run]";//操作行為  
  212.                 TXT += "[Error]";//操作行為  
  213.                 TXT += "[WatcherPath is not exists.]";  
  214.                 LFM.writeLog(TXT);  
  215.                 return; } //確認監視路徑存不存在,不存在則不啟動監視  
  216.             if (!BackupPath.Exists)  
  217.             {  
  218.                 try  
  219.                 {  
  220.                     BackupPath.Create();//備份的路徑不存在則建立  
  221.                 }  
  222.                 catch (IOException e)  
  223.                 {  
  224.                     TXT = "";  
  225.                     TXT += "[" + ClassNickName + "]"; //寫入監視名稱  
  226.                     TXT += "[Watcher Run]";//操作行為  
  227.                     TXT += "[Error]";  
  228.                     TXT += "["+e.Message+"]";  
  229.                     LFM.writeLog(TXT);  
  230.                 }  
  231.   
  232.                 if (!BackupPath.Exists) { return; }//確認是否建立成功,失敗則不啟動監視  
  233.             }  
  234.             //啟動前比對資料的平衡  
  235.             ComparisonCatchUp();  
  236.             _watch.EnableRaisingEvents = true;  //啟動監視  
  237.   
  238.              TXT = "";  
  239.             TXT += "[" + ClassNickName + "]"; //寫入監視名稱  
  240.             TXT += "[Watcher Run]";//操作行為  
  241.             LFM.writeLog(TXT);  
  242.         }  
  243.   
  244.         /**
  245.          *將Ref與Buf同步,以Ref主
  246.          */  
  247.         public void ComparisonCatchUp()  
  248.         {  
  249.             //private DirectoryInfo WatcherPath, BackupPath;  
  250.             //  
  251.             if (WatcherPath == null || BackupPath == null) { closed("[Path is Null]"); return; }  //路徑錯誤關閉監視並離開  
  252.             if(!WatcherPath.Exists) { closed("[WatcherPath is not exists]"); return; }  //監視的路徑不存在關閉監視並離開  
  253.             if (!BackupPath.Exists) { BackupPath.Create(); }  //備份的路徑不存在則建立  
  254.                                                               // FileSystemInfo[] fsIarry= WatcherPath.GetFileSystemInfos();  
  255.             _Remove_Over_FileSystemInfo(WatcherPath,BackupPath);  
  256.             //取得監視路徑以及備份路徑下所有資料  
  257.             //逐一比對  
  258.             _List_CatchUp(WatcherPath);  
  259.             System.GC.Collect();  
  260.             //MessageBox.Show("_CatchUp off");  
  261.         }  
  262.   
  263.         private void _CatchUp(FileSystemInfo target)  
  264.         {  
  265.             if (!(File.Exists(target.FullName) || Directory.Exists(target.FullName))) { return; }  
  266.   
  267.             if (File.Exists(target.FullName))//是檔案  
  268.             {  
  269.                 FileInfo fa = new FileInfo(target.FullName);  
  270.                 string difference= comparison.ExcludeTitle(fa, WatcherPath);  
  271.                 FileInfo fb = new FileInfo(BackupPath.FullName + difference);  
  272.   
  273.                 if (fb.Exists)//確認BackupPath裡的對應檔案是否存在  
  274.                 {  
  275.                     if (comparison._StillFolderFile(fa, fb))  
  276.                     {  
  277.                         return; //檔案相同不需改變,離開  
  278.                     }  
  279.                     else  
  280.                     {  
  281.                         //檔案不相同覆蓋BackupPath的檔案  
  282.                         _Delete_FileSystemInfo(fb.FullName);      
  283.                         fa.CopyTo(fb.FullName);                         
  284.                     }  
  285.                 }  
  286.                 else  
  287.                 {  
  288.                     //不存在則備份到BackupPath  
  289.                     fa.CopyTo(fb.FullName);  
  290.                    
  291.                 }  
  292.             }  
  293.             else  
  294.             {  
  295.                 //是資料夾  
  296.                
  297.                 DirectoryInfo da = new DirectoryInfo(target.FullName);  
  298.                 string difference = comparison.ExcludeTitle(da, WatcherPath);  
  299.                 DirectoryInfo db = new DirectoryInfo(BackupPath.FullName + difference);  
  300.   
  301.                 if (db.Exists) //確認BackupPath裡的對應檔案是否存在  
  302.                 {  
  303.                     //刪除BackupPath(db)裡多餘的檔案,以da為依據  
  304.                     _Remove_Over_FileSystemInfo(da, db);  
  305.                
  306.                     //相同檔名比對檔案是否不相同  
  307.                     if (!comparison._StillFolderFile(da, db))  
  308.                     {  
  309.                         //不相同則進行探掘  
  310.                         _List_CatchUp(da);  
  311.                         //_CatchUp(da);     
  312.                     }  
  313.                 }  
  314.                 else  
  315.                 {  
  316.                     //不存在則建立到BackupPath  
  317.                     db.Create();  
  318.                     _List_CatchUp(da);  
  319.                 }  
  320.   
  321.             }  
  322.   
  323.         }  
  324.         //刪除檔案或資料夾  
  325.         private void _Delete_FileSystemInfo(String path)  
  326.         {  
  327.             try  
  328.             {  
  329.                 if (File.Exists(path))   //是檔案則直接刪除  
  330.                 {  
  331.                     File.Delete(path);  
  332.                     //MessageBox.Show("_Delete_FileSystemInfo_Delete(" + path + ");");  
  333.                 }  
  334.                 else  
  335.                 {  
  336.                     /*
  337.                        String[] bufferList = Directory.GetFileSystemEntries(path);
  338.                        for (int index=0; index<bufferList.Length; index++)
  339.                        {
  340.                            _Delete_FileSystemInfo(bufferList[index]);  //探掘並清除內部資料
  341.                            String TXT = "";
  342.                             TXT += "[" + ClassNickName + "]"; //寫入監視名稱
  343.                             TXT += "[Delete]";//操作行為
  344.                             TXT += "<" + bufferList[index] + ">";
  345.                             LFM.writeLog(TXT);
  346.                        }
  347.                        */  
  348.                     Directory.Delete(path, true);//刪除資料夾  
  349.                 }  
  350.                 String TXT = "";  
  351.                 TXT += "[" + ClassNickName + "]"; //寫入監視名稱  
  352.                 TXT += "[Delete]";//操作行為  
  353.                 TXT += "<" + path + ">";  
  354.                 LFM.writeLog(TXT);  
  355.             }  
  356.             catch (IOException e)  
  357.             {  
  358.                 String TXT = "";  
  359.                 TXT += "[" + ClassNickName + "]"; //寫入監視名稱  
  360.                 TXT += "[Delete]";//操作行為  
  361.                 TXT += "[ERROR][" + e.Message + "]"; //寫入Exception  
  362.                 TXT += "<" + path + ">";  
  363.                 LFM.writeLog(TXT);  
  364.             }  
  365.         }  
  366.   
  367.         private void _List_CatchUp(FileSystemInfo target)  
  368.         {  
  369.             String[] dirfilrList = Directory.GetFileSystemEntries(target.FullName);  
  370.             for (int dirfilr = 0; dirfilr < dirfilrList.Length; dirfilr++)  
  371.             {  
  372.                 _CatchUp(new DirectoryInfo(dirfilrList[dirfilr]));  
  373.             }  
  374.         }  
  375.   
  376.         private void _Remove_Over_FileSystemInfo(FileSystemInfo refFSI, FileSystemInfo WasChanged)  
  377.         {  
  378.             //刪除BackupPath裡多餘的檔案  
  379.             String[] daList = Directory.GetFileSystemEntries(refFSI.FullName);  
  380.             String[] dbList = Directory.GetFileSystemEntries(WasChanged.FullName);  
  381.          
  382.             for (int index_b = 0; index_b < dbList.Length; index_b++)  
  383.             {  
  384.                 //MessageBox.Show("_Remove_Over_FileSystemInfo(LOOP_B" + dbList[index_b] + ");");  
  385.                 string diff_b = comparison.ExcludeTitle(dbList[index_b], BackupPath.FullName); //取得去除BackupPath title的檔名  
  386.                 bool delkey = true;  
  387.                 for (int index_a = 0; index_a < daList.Length; index_a++)  
  388.                 {  
  389.                     string diff_a = comparison.ExcludeTitle(daList[index_a], WatcherPath.FullName); //取得去除WatcherPath title的檔名  
  390.   
  391.                     if (diff_b.Equals(diff_a)) { delkey = false; }   //相等則跳出index_"b" 這次的比對  
  392.                 }  
  393.   
  394.                 if (delkey)  
  395.                 { //找不到相等的檔名,判斷為多餘的檔案,刪除BackupPath裡的此檔案  
  396.                     _Delete_FileSystemInfo(dbList[index_b]);  
  397.                     //MessageBox.Show("_Delete_FileSystemInfo(" + dbList[index_b] + ");");  
  398.                 }  
  399.   
  400.             }  
  401.         }  
  402.           
  403.         public override String ToString()  
  404.         {  
  405.             return "{["+ClassNickName+"],["+ WatcherPath.ToString()+"],["+ BackupPath .ToString()+"],["+ FilterType.ToString() + "]}";  
  406.         }  
  407.   
  408.     }  
  409. }  

引用網址:https://home.gamer.com.tw/TrackBack.php?sn=3707803
All rights reserved. 版權所有,保留一切權利

相關創作

同標籤作品搜尋:C#|ThreadPool|FileSystemEventHandler

留言共 6 篇留言

透明
不明覺厲

09-02 16:09

碎銀子
就是要把事情講得很複雜才能騙人!09-02 16:13
無名氏
程式碼是不是只有一部分而已?
我這邊找不到DynamicFileSystemInfoControl、LogFileManagement跟Comparison
這些物件是你自己建立的,還是是別的Library的東西?

我發現建構子那邊都是重複的動作,我個人習慣會寫成像這樣
前兩個參數較少的只是做包裝,主要的工作都是在參數最完整的那一個
public FileFolderWatcher(DirectoryInfo Watcher_Path, DirectoryInfo Backup_Path)
: this("FileFolderWatcher", Watcher_Path, Backup_Path, "*.*") {
}
public FileFolderWatcher(String NickName, DirectoryInfo Watcher_Path, DirectoryInfo Backup_Path)
: this(NickName, Watcher_Path, Backup_Path, "*.*") {
}
public FileFolderWatcher(String NickName, DirectoryInfo Watcher_Path, DirectoryInfo Backup_Path, String Filter) {
ClassNickName = NickName;
WatcherPath = Watcher_Path;
BackupPath = Backup_Path;
FilterType = Filter;
FileWatcher();
}

迴圈的部分用foreach也是滿方便的一個作法,例如_List_CatchUp()這邊可以改成
String[] dirfilrList = Directory.GetFileSystemEntries(target.FullName);
foreach (string dirfilr in dirfilrList) {
_CatchUp(new DirectoryInfo(dirfilr));
}
適合用在要把整個陣列跑一次的作法

09-02 18:32

碎銀子
對的這是局部程式,我晚點我放完整的好09-02 18:36
碎銀子
建構子的確是應該用你說的方式,只是因為C#的寫法跟JAVA不一樣,所以一時間我不知道怎麼用
09-02 18:38
碎銀子
C#我是剛好公司環境不適合跑JAVA,所以我才去翻C#指令來寫,在前一個程式之前我完全沒碰過C#,如果還有其他可以改善請多多打臉!打腫消下去就是實力了09-02 18:42
無名氏
建構子這部分Java跟C#還滿像的,有時候恍神就會打錯了[e8]
例如下面個程式碼,只有this擺得位置不一樣

C#:
public class Pt {
private int x, y;

public Pt() : this(0, 0) {
}
public Pt(int x, int y) {
this.x = x;
this.y = y;
}
}

Java:
public class Pt {
private int x, y;

public Pt() {
this(0, 0);
}
public Pt(int x, int y) {
this.x = x;
this.y = y;
}
}

09-02 19:09

碎銀子
我一開始用JAVA的寫法被IDE顯示錯誤,就想說程式碼差不多就直接用複製貼上改一下而已,對了完整檔案我放上去了09-02 19:17
無名氏
有看到你有使用到ini檔,不過好像是自訂格式而非常見的格式
我用NuGet裝了一個叫ini-parser的套件,可以讀寫標準格式的ini檔

我改了readIniFile()、writeIniFile()跟LogFileManagement,程式碼在這
https://gist.github.com/anonymous/bc6a268d3a15a15891f8d99180b79e96

原本的ini.ini也改成標準的ini格式了,放在程式碼最上方

09-03 23:48

碎銀子
謝謝幫忙修正,我有一處想詢問,就是為什麼物件的鎖定是宣告lock (new object())而不是lock (this)?09-04 00:08
無名氏
因為WriteLog()是靜態方法(static),不能用this
直接使用一個object類型的變數當作lock是我個人常用的做法,所以就寫成那樣了

lock 陳述式 (C# 參考)
https://docs.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/lock-statement

09-04 00:17

碎銀子
瞭解了!謝謝
09-04 00:25
貓貓風 ฅ●ω●ฅ
有空來研究研究OAO

09-23 02:29

碎銀子
歡迎多多來打臉OAO09-30 14:09
我要留言提醒:您尚未登入,請先登入再留言

3喜歡★weino1101 可決定是否刪除您的留言,請勿發表違反站規文字。

前一篇:檔案轉存... 後一篇:新年新希望!~~巴魯斯...

追蹤私訊切換新版閱覽

作品資料夾

tyu15826大家
羽澤鶇的扭曲仙境——跳舞人偶與夢幻遊樂園 19藍組紀錄(四) 更新看更多我要大聲說昨天18:41


face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】