2021年3月5日 星期五

php 同時傳送超過1000個變數時的做法


php 同時傳送超過1000個變數時,比方
<form id="myForm">
<?php
for ($i=0; $i <1200 ; $i++) {
  echo '<button type="submit">送出</button>';
}
?>
<form>
此時會報錯誤
PHP Warning:  Unknown: Input variables exceeded 1000. To increase the limit change max_input_vars in php.ini.
雖然可以照它說的去更改 max_input_var 的數字,但終歸無法一牢永逸(因為我的變數一直在增加)
所以上網找到這個解法:將1000個變數的值集中到一個變數中,接收後再拆開來使用


在 html 的部份,加上一變數,在送出前集中所有變數的值到此一變數
<input name="storeids" type="hidden" />
<button type="submit" onclick="changeidstostring();">送出</button>

在 js 的部份這樣寫
function changeidstostring() {
  $('#myForm').submit(function() {
    var storeids = $("input[name='ids[]']").map(function(){return $(this).val();}).get();
    $("input[name='storeids']").val(storeids);
    $("input[name='ids[]']").attr("disabled","disabled");	// 將原來會產生很多變數的元素disabled 掉,就不會傳送了 
  });
}
在接收的部分這樣寫
$ids = explode(",", $_POST['storeids']);  // 因為參數太多(超過1000個,所以改成先集中到某個參數,傳送過來再拆解)

2019年8月27日 星期二

PHP PDO IN 條件

使用PHP PDO 在組合查詢語法時若有 IN 條件,通常會用此方法
$myArray = array(1,2,3);
$in = trim(str_repeat('?,', count($myArray)),',');
$stm = $conn->prepare("SELECT * FROM table WHERE id IN ($in)");
$stm->execute($myArray);
萬一遇上 $myArray 的個數很多時,會造成語法太長,無法執行怎麼辦?
試試下面這個方法
$myArray = array(1,2,3);
$in = implode(',', $myArray);
$stm = $conn->prepare("SELECT * FROM table WHERE FIND_IN_SET(id, :inArray) ORDER BY name");
$stm->bindParam('inArray', $in);
$stm->execute();
如果是 not in,就加上一驚嘆號,變成 !FIND_IN_SET 就可以了
$myArray = array(1,2,3);
$in = implode(',', $myArray);
$stm = $conn->prepare("SELECT * FROM table WHERE !FIND_IN_SET(id, :inArray) ORDER BY name");
$stm->bindParam('inArray', $in);
$stm->execute();

2019年7月9日 星期二

使用 Gmail 的兩步驟認證下,透過 PHPMailer 發送郵件範例

到google 設定兩步驟認證
1.登入google後,找到 google帳戶,在"安全性"下會看到如下圖
未啟用兩步驟驗證的畫面

2.選擇上圖中的"兩步驟驗證"後,按下"開始使用"後,依照步驟操作完成,會得到一組16位數的密碼,此密碼會用於步驟三中的
$mail->Password = '密碼';

程式碼配置與發信程式撰寫(在沒有 composer 的環境下)
一、程式碼下載
1.到 github 上下載程式碼:PHPMailer,並解壓縮
解壓縮後資料夾內容
2.將 PHPMailer 資料夾放在自己知道的路徑下,比方:/var/www/mySite/PHPMailer

二、發信程式碼撰寫
<?php
try {
   /* Set the mail encode. */
   CharSet = 'UTF-8';

   /* Set the mail sender. */
   $mail->setFrom('寄件人@gmail.com', '寄件人名稱');

   /* Add a recipient. */
   $mail->addAddress('收件人郵件地址', '收件人名稱');

   /* Set the subject. */
   $mail->Subject = '主旨';

   /* Set the mail message body. */
   $mail->Body = '信件內容';

   /* SMTP parameters. */
   
   /* Tells PHPMailer to use SMTP. */
   $mail->isSMTP();
   
   /* SMTP server address. */
   $mail->Host = 'smtp.gmail.com';

   /* Use SMTP authentication. */
   $mail->SMTPAuth = TRUE;
   
   /* Set the encryption system. */
   $mail->SMTPSecure = 'tls';
   
   /* SMTP authentication username. */
   $mail->Username = '寄件人@gmail.com';
   
   /* SMTP authentication password. google 兩步驟認證的密碼*/
   $mail->Password = '密碼';
   
   /* Set the SMTP port. */
   $mail->Port = 587;

   /* Finally send the mail. */
   $mail->send();

   echo "成功發送信件!";
}
catch (Exception $e)
{
   echo "發送失敗!";
   /* PHPMailer exception. */
   echo $e->errorMessage();
}
catch (\Exception $e)
{
   echo "發送失敗!";
   /* PHP exception (note the backslash to select the global namespace Exception class). */
   echo $e->getMessage();
}
?>

https://alexwebdevelop.com/phpmailer-tutorial/ 阿力獅的教室

2019年3月28日 星期四

php 將接收 POST 來的資料放到 html 元素中要注意的地方

事情是這樣的,因為某些因素,要 POST 的資料包含 HTML 中的 TAG 符號,如下
$str = "<option value="1">student</option><option value="2">teacher</option><option value="3">teachers' office</option>";
在接收資料時,要將它還原成一下拉選單,如下
<select id="select_1">
<option value="1">student</option>
<option value="2">teacher</option>
<option value="3">teachers' office</option>
</select>
但是因為文字中有特殊符號  ' ,造成只能顯示
<select id="select_1">
<option value="1">student</option>
<option value="2">teacher</option>
<option value="3">teachers

上網 google 了一下後,修改方式:
在 POST 前用 htmlentities 將資料包起來,這樣接收時就可以正常了


[PHP] HTML特殊字元轉換

2019年1月29日 星期二

php 的日期比較

在比對之前,先說明一下如何得到目前日期 在 php 5.2(含) 前可以用 date('Y-m-d') 在 php 5.2(不含) 後可以用 new DateTime() 雖然函數有一點點不同,但都需要注意時區可能產生的問題

接下來開始比較日期
使用函數 strtotime
$Date1 = date('Y-m-d'); 
$Date2 = '9999-12-31';
if(strtotime($Date1) > strtotime($Date2)) { 
  echo 'Date1 較新'; 
} 
else { 
  echo 'Date2 較新'; 
} 
基本上這樣就可以了,但是若是使用XAMPP,上面的語法可能會出現錯誤,原因在於 Y2K38漏洞,也被稱為Unix Millennium Bug,也就是 strtotime 只認得 2038-1-19 03:14:07 之前的日期時間,在此日期時間後的就會溢位,進而產生錯誤。

所以將語法修改如下,順便用了 php 5.2 版之後的語法
$Date1 = date_format(new DateTime(),'U'); 
// $Date2 = date_format(new DateTime('9999-12-31'),'U');
// 這邊需要注意因為 DateTime()會包含時間的部分,所以在比較時要同時加上時間的部分(或去除時間的部分)來比較會較正確,
// 或是將 $Date2 多加一天後再比較也行,所以將 $Date2 修改一下
$Date2 = date_format(date_modify(new DateTime('9999-12-31'),'+1 days'),'U');
if($Date1 > $Date2) { 
  echo 'Date1 較新'; 
} 
else { 
  echo 'Date2 較新'; 
} 
藍色小舖 php 討論區- 日期的比較 PHP轉換超過2038年的日期出錯問題解決

用Temp Table取代Cursor

cursor通常用來逐筆處理資料使用,下面是一個簡單的範例(MS SQL Server 2008)
declare @myId int
declare @myName nvarchar(20)
declare @myCursor CURSOR

set @myCursor = CURSOR FAST_FORWARD
FOR
SELECT ID, NAME FROM Employee
open @myCursor
INTO @myId, @myName
WHILE @@FETCH_STATUS = 0
BEGIN

    --DO SOMETHING

    FETCH NEXT FROM @myCursor
    INTO @myId, @myName

END

CLOSE @myCursor
DEALLOCATE @myCursor
用temp table來模擬cursor的操作模式,簡單的說就是把SELECT ID, NAME FROM Employee的結果存到temp table中,在逐步讀取,所以必須先建立一個temp table如下
create table #tempEmployee
(
    ID int,
    NAME nvarchar(20)
)
然後把資料insert into select 到 #tempEmployee
insert into #tempEmployee (ID,NAME)
select ID,NAME
from Employee
接下來,就是逐步讀取這個temp table #tempEmployee
在MS SQL Server中使用SET ROWCOUNT 1來控制select時一次只撈出一筆資料,而且每做完一筆就刪掉,直到#tempEmployee沒有資料為止
declare @countTemp int --用來計算#tempEmployee還剩幾筆資料

--計算#tempEmployee資料數
select @countTemp = count(*) from #tempEmployee

while(@countTemp > 0)
begin
    set rowcount 1
    select @myId = ID, @myName = NAME from #tempEmployee
    --To Something
     
    --因為set rowcount 1的關係,所以一次只會刪一筆
    delete from #tempEmployee 

    --計算還剩幾筆,@countTemp > 0繼續
    select @countTemp = count(*) from #tempEmployee  
end

--#刪除 tempEmployee
drop table #tempEmployee

--把select的預設比數恢復正常
set rowcount 0
如此一般,完整的程式碼如下
declare @myId int
declare @myName nvarchar(20)

create table #tempEmployee
(
    ID int,
    NAME nvarchar(20)
)

declare @countTemp int --用來計算#tempEmployee還剩幾筆資料

--計算#tempEmployee資料數
select @countTemp = count(*) from #tempEmployee

while(@countTemp > 0)
begin
    set rowcount 1
    select @myId = ID, @myName = NAME from #tempEmployee
    --To Something
     
    --因為set rowcount 1的關係,所以一次只會刪一筆
    delete from #tempEmployee 

    --計算還剩幾筆,@countTemp > 0繼續
    select @countTemp = count(*) from #tempEmployee  
end

--#刪除 tempEmployee
drop table #tempEmployee
遜砲賴的爆肝筆記-stored procedure中不使用cursor逐步讀取資料列的方法

2018年4月29日 星期日

不爽 0002

總: 董事長剛吃完飯,等等讓他吃藥
我: 好(看了一下桌面),只有一個杯子?
總: 對,你到底有沒有在顧(用心)?飯後都只有一杯藥
我: 有啊(心想早上明明看到2個杯子的藥,還是我看錯了?)
(之後)
總: 你...
我: 嗯(不想回答有意義的文字了)


(中午,問了一下護理師)
我: 請問早上餐後的藥是一杯還兩杯?
護: 兩杯呵,一杯裡面三顆藥,一杯喝的
我: 謝謝。(心想,掯!早上總經理是在唸不爽的嗎?到底是誰沒在顧?)

不爽 0001

早上總經理帶來早餐(一袋兩個餐+另一袋一杯飲料)
總: 看你要吃什麼餐
我: 隨手拿了一個吃完後
總: 有飲料
我: 打開一看只有一杯,你喝什麼?
總: 你喝
(我拿出來時不小心打翻飲料,桌上地上都有)
總: 龜龜毛毛(一手拿早餐,一手拿在處理飲料)
我: 我自己處理
總: 不用,代誌都不會做
(我待了幾秒後,就不爽的離開)
(過一會兒,總經理來電,我不想接,直接回來)
總: 突然跑去哪裡?
我: (沒回應,因為不爽,心想有必要這樣罵人嗎?)

2018年4月3日 星期二

Windows Form 將 FormBorderStyle 設為 none 時,視窗的拖曳方式

因為專案畫面上的需求(Form 表頭的顏色與設計),
所以將 Windows Form 的 FormBorderStyle 設定 none 後,再自行加上 TableLayoutPanel (tableLayoutPanel1) 等控制項來偽裝

此時如果遇上使用者使用的電腦解析度,低於設計時,會造成畫面被切掉,此時因為 FormBorderStyle 設為 none,所以 Form 也拉不動,所以需要再對於拖曳功能加工(沒事找事做)
對於 Form 內的控制項可以單純的在 MouseDown(按下滑鼠)、DragEnter(拖曳開始)、DragDrop(拖曳結束) 事件中寫下相對應的語法,可參考 在 Windows Form 中執行拖放作業

但現在是在 Windows Form 外執行拖放作業,可以用win32 的角度來撰寫語法
// win32 的角度來撰寫語法
public const int WM_NCLBUTTONDOWN = 0xA1;
public const int HT_CAPTION = 0x2;

[System.Runtime.InteropServices.DllImportAttribute("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
[System.Runtime.InteropServices.DllImportAttribute("user32.dll")]
public static extern bool ReleaseCapture();

// 再在 tableLayoutPanel1 的 MouseDown 事件寫下
private void tableLayoutPanel1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{     
    if (e.Button == MouseButtons.Left)
    {
        ReleaseCapture();
        SendMessage(Handle, WM_NCLBUTTONDOWN, HT_CAPTION, 0);
    }
}
或是在 tableLayoutPanel1 的 MouseDown、MouseMove 事件撰寫語法
// 在 tableLayoutPanel1 的 MouseDown、MouseMove 事件撰寫語法
private Point startPoint;   // 紀錄目前視窗的位置
private void tableLayoutPanel1_MouseDown(object sender, MouseEventArgs e)
{
    //當滑鼠擊以左點擊控制項的範圍內時,透過計算紀錄目前視窗的位置
    if (e.Button == MouseButtons.Left)
    {
        startPoint = new Point(-e.X + SystemInformation.FrameBorderSize.Width, -e.Y - SystemInformation.FrameBorderSize.Height);
    }
}

private void tableLayoutPanel1_MouseMove(object sender, MouseEventArgs e)
{
    // 當滑鼠擊按著左鍵移動時,記錄下移動的位置
    if (e.Button == MouseButtons.Left)
    {
        // 滑鼠指標的位置
        Point mousePos = Control.MousePosition;
        // 新視窗的位置(等於滑鼠指標目前的位置與先前視窗位置的位移)
        mousePos.Offset(startPoint.X, startPoint.Y);
        // 改變視窗位置
        Location = mousePos;
    }
}

2018年3月2日 星期五

BackgroundWorker 的使用

專案需求:一支常駐程式可以撈資料
想法大概是:一個 Timer 定期檢查、一個 ProgressBar 顯示進度、一個 backgroundworker 避免影響使用者、一個 NotifyIcon 可以出現桌面的右下角
程式寫法大致上是:

新增一個 Form,在上面放一個 ProgressBar
然後拉一個 Timer
一個 BackgroundWorker,並設定 WorkerReportsProgress = true,才可以修改 ProgressBar 的進度 然後在初始化後設定 form 的位置
public Form1()
{
    InitializeComponent();

    // 縮小視窗,以觸發 notifyIcon (不直接觸發是為了保留之後若要點擊縮小的 Icon 時可以恢復視窗)
    this.WindowState = FormWindowState.Minimized;
    this.TopLevel = true;
    this.screenWidth = Screen.PrimaryScreen.Bounds.Width;
    this.screenHeight = Screen.PrimaryScreen.Bounds.Height;
    //this.Location = new Point(screenWidth - Width - 5, screenHeight);

    //設定視窗位置在右下角最高可以顯示的位置, 工具列上方
    this.stopHightLocation = Screen.PrimaryScreen.Bounds.Height - (Screen.PrimaryScreen.Bounds.Height - Screen.PrimaryScreen.WorkingArea.Height + this.Height);
    this.Location = new Point(screenWidth - Width - 5, stopHightLocation);
}
在 BackgroundWorker (Name 設為 bw) 的事件上分別寫上
void bw_DoWork(object sender, DoWorkEventArgs e)
{
    object myParameter = e.Argument; // myParameter 是帶過來要用的參數
    // 開始執行要很久的動作
    ...(略)
    // 結束
}
void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    // 把視窗叫起來讓使用者看到進度條
    if (this.WindowState == FormWindowState.Minimized)
    {
        this.Show();    // 顯示視窗
        this.WindowState = FormWindowState.Normal;
    }
    // 修改進度條的進度
    progressBar1.Value = e.ProgressPercentage;
    // 以文字的方式顯示進度條的百分比
    label2.Text = string.Format("{0:f0}%", (progressBar1.Value / Convert.ToDouble(progressBar1.Maximum)) * 100);
}
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    this.WindowState = FormWindowState.Minimized;   // 隱藏視窗
    timer1.Enabled = true;  // 工作完成了,再次啟用 timer1
}
再來在 form load 事件內啟用 timer1
private void timer1_Tick(object sender, EventArgs e)
{
    // 如果背景程式忙碌中,就停止這次的背景
    if (bw.IsBusy)
        return;

    timer1.Enabled = false;  // 然後將 timer 停用

    // 這邊可以收集要的參數
    // 若收集參數也很耗時,則可以另開一個backgroundworker來做,然後在backgroundworker_completed事件內執行主要的忙碌工作
    // 或是將收集參數的工作也被到bw裡面,但此時可能不單純只顯示progressbar,可能還需要顯示進度的內容說明

    // 然後呼叫backgroundworker 執行 (可以加判斷是否真的需要呼叫背景程式,減少觸發次數)
    if (myParameter != null)
        bw.RunWorkerAsync(myParameter); 
}
後來看到另一篇文章寫到,原來也可以在 BackgroundWorker_Completed 事件中,再次執行耗時的工作即可,減少使用一個 timer 元件,
把timer1_Tick()事件用另一個function 包起來,然後稍微修改一下
void PrepareToRunWorkerAsync()
{
    // 如果背景程式忙碌中,就停止這次的背景
    if (bw.IsBusy)
        return;

    // 這邊可以收集要的參數
    // 若收集參數也很耗時,則可以另開一個backgroundworker來做,然後在backgroundworker_completed事件內執行主要的忙碌工作
    // 或是將收集參數的工作也被到bw裡面,但此時可能不單純只顯示progressbar,可能還需要顯示進度的內容說明

    // 然後呼叫backgroundworker 執行
    bw.RunWorkerAsync(myParameter); 
}
然後在 Form1() 建構式中直接執行上述的 function
public Form1()
{
    InitializeComponent();

    // 縮小視窗,以觸發 notifyIcon (不直接觸發是為了保留之後若要點擊縮小的 Icon 時可以恢復視窗)
    this.WindowState = FormWindowState.Minimized;
    this.TopLevel = true;
    this.screenWidth = Screen.PrimaryScreen.Bounds.Width;
    this.screenHeight = Screen.PrimaryScreen.Bounds.Height;
    //this.Location = new Point(screenWidth - Width - 5, screenHeight);

    //設定視窗位置在右下角最高可以顯示的位置, 工具列上方
    this.stopHightLocation = Screen.PrimaryScreen.Bounds.Height - (Screen.PrimaryScreen.Bounds.Height - Screen.PrimaryScreen.WorkingArea.Height + this.Height);
    this.Location = new Point(screenWidth - Width - 5, stopHightLocation);

    PrepareToRunWorkerAsync();
}
修改一下 BackgroundWorker_Completed 事件
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    this.WindowState = FormWindowState.Minimized;   // 隱藏視窗
    // 若使用者沒有下取消命令,則在Completed 事件下再次執行耗時的工作
    if (e.Cancelled == false)
        PrepareToRunWorkerAsync();

}
當然上述做法需再搭配一個啟動耗時工作的事件,不然一但取消後,就只有重開執行檔一途了
void buttonStart(object sender, EventArgs e)
{
    PrepareToRunWorkerAsync();
}
參考:C# 使用 BackgroundWorker 背景執行

2018年2月23日 星期五

動態控制 ContextMenuStrip 不出現

通常在按下滑鼠右鍵要出現選單時,都會用到 ContextMenuStrip 這個控制項,使用方式如下:
1.從工具箱點兩下 ContextMenuStrip,產生 contextMenuStrip1
2.點擊該 contextMenuStrip1,可以編輯清單內容
3.選擇 contextMenuStrip1 要出現在哪個控制項,在該控制項的 ContextMenuStrip 屬性上選擇 contextMenuStrip1 

但是當要動態控制 contextMenuStrip1 要不要出現時,
無法寫成
// 假設 contextMenuStrip1 要出現在 treeView1 的右鍵時
treeView1.ContextMenuStrip = "";
只能在 contextMenuStrip1.Opening 事件上動手腳
boolean bContextMenuStripVisible = false; // 指定是否顯示 ContextMenuStrip
private void contextMenuStrip1_Opening(object sender, CancelEventArgs e)
{
    if (bContextMenuStripVisible == false)
    {               
          e.Cancel = true;
    }
}

2018年2月22日 星期四

TableLayoutPanel 內的控制項的 Dock 屬性無法完全作用

TableLayoutPanel 是可以讓控制項排列整理的一個控制項(有點繞舌) 因專案需要,在 TableLayoutPanel 內塞了幾種控制項(Label、TextBox、ListBox)

然後把TableLayoutPanel 內的控制項的 Dock 屬性設定為 Fill,塞滿格子

有沒有發現
TextBox 的 Dock 設為 Fill 後,沒完全 Work?
原來是因為 TextBox 的 MultiLine 是 False 的關係,因為只能有一行字,當然無法撐開控制項的高度,所以修改的方式有兩種,視需求選擇使用
1. 改變字型大小
2. 把 MultiLine 設為 True

ListBox 的 Dock 設為 Fill 後,沒完全 Work?
原來 ListBox 有一個屬性 IntegralHeight(表示清單是否可以只包含完整的項目),意思應該是指 ListBox 的高度不足以顯示完整的 Item 時,只會展開到完整顯示的 Item 的高度,所以只要將 IntegralHeight 設為 False,就可以達到 Dock=Fill 的完整效果了

如果要將控制項完全貼合 TableLayoutPanel,則調整內部的各控制項的 Margin 屬性即可

2018年2月9日 星期五

NChar 與 Char 的差別

簡言之

NChar 是以位元組(2個位元)的方式儲存資料(NVarChar 亦同)
Char 是以位元的方式儲存資料(VarChar 亦同)

說明:
位元組:可以儲存半型字 或 全型字
位元:僅可以儲存半型字

有點雙人床與單人床的概念
雙人床:可以睡 1 個人或是 2 個人
單人床:僅可以睡 1 個人

所以當你要存的資料
只包含 英文、數字,即半型字,可以用 Char 即可
只包含 中文,即全型字,也可以用 Char 但記得長度要開 2 倍,或是直接用 NChar,長度就不用開到 2 倍

舉例:
以下語法會出現 字串或二進位資料會被截斷 的訊息
因為資料"五"是一個全型字,而欄位Char(1)僅能存一個半型字
declare @MyTable Table (ColName Char(1))
insert into @MyTable Values ('五')
改用 NChar
-- 改用 NChar
declare @MyTable Table (ColName NChar(1))
insert into @MyTable Values ('五')
或是改成 2 倍長度
--改成 2 倍長度 Char(2)
declare @MyTable Table (ColName Char(2))
insert into @MyTable Values ('五')
就OK了

若是 中文、英文、數字夾雜,那就看個人喜愛了



另外提供兩個SQL函數(Len、DataLength),可以幫助釐清上面的差別
將語法改成如下:
將欄位長度改成 3
-- 在 Char 的情況下
declare @MyTable Table (ColName Char(3))
insert into @MyTable Values ('五')
select *, Len(ColName), DATALENGTH(ColName) from @MyTable
-- 結果:
-- Len(ColName) = 1,即資料的字元數
-- DATALENGTH(ColName) = 3,即欄位的位元數
-- 在 NChar 的情況下
declare @MyTable Table (ColName NChar(3))
insert into @MyTable Values ('五')
select *, Len(ColName), DATALENGTH(ColName) from @MyTable
-- 結果:
-- Len(ColName) = 1,即資料的字元數
-- DATALENGTH(ColName) = 6,即欄位的位元數

再來,將資料內容修改一下(儲存一個全型字和一個半型字)
-- 在 Char 的情況下
declare @MyTable Table (ColName Char(3))
insert into @MyTable Values ('五5')
select *, Len(ColName), DATALENGTH(ColName) from @MyTable
-- 結果:
-- Len(ColName) = 2,即資料的字元數
-- DATALENGTH(ColName) = 3,即欄位的位元數
-- 在 NChar 的情況下
declare @MyTable Table (ColName NChar(3))
insert into @MyTable Values ('五5')
select *, Len(ColName), DATALENGTH(ColName) from @MyTable
-- 結果:
-- Len(ColName) = 2,即資料的字元數
-- DATALENGTH(ColName) = 6,即欄位的位元數

2018年1月19日 星期五

還原TreeView 的展開狀態

例:
 
如果在 A 下的 C 下加入 D,如果只是在 TreeView 上改變顯示,那麼要遍歷整個 Tree,找出所有的 C (B 下有一個 C)之後,改變畫面,雖然可以這樣做,但程式碼上似乎複雜了點
所以改變思路,在 C 下加入 D 之後,重新綁定 TreeView 的資料。

但是重新綁定 TreeView 之後,TreeView 會收合到剩下第一層,使用者就要重新展開之後,再往下加入其他子項目,這點其實很麻煩。

所以在重新綁定前先記錄下目前 TreeView 的展開狀態,綁定之後再還原展開狀態,程式碼加下

設定兩個全域變數
private Dictionary<string, bool> NodesStatus = new Dictionary<string, bool>();  // 記錄展開狀態
private string SelectNodeFullPath = ""; // 記錄選擇到的節點

記錄展開狀態
/// <summary>
/// 記錄展開狀態
/// </summary>
/// <param name="nodes"></param>
private void GetTreeNodesStatus(TreeNodeCollection nodes)
{
 foreach (TreeNode node in nodes)
 {
  if (node.IsExpanded)
  {
   NodesStatus[node.FullPath] = true;
  }
  else
  {
   NodesStatus.Remove(node.FullPath);
  }

  if (node.IsSelected)
  {
   SelectNodeFullPath = node.FullPath;
  }
  GetTreeNodesStatus(node.Nodes);
 }
}

還原展開狀態
/// <summary>
/// 還原展開狀態
/// </summary>
/// <param name="nodes"></param>
private void SetTreeNodesStatus(TreeNodeCollection nodes)
{
 foreach (TreeNode node in nodes)
 {
  if (NodesStatus.ContainsKey(node.FullPath))
  {
   node.Expand();
  }

  if (node.FullPath == SelectNodeFullPath)
  {
   treeView1.SelectedNode = node;
  }

  SetTreeNodesStatus(node.Nodes);
 }
}

使用方式
// 對treeView1做了某些操作之後(例如加入子選項)
GetTreeNodesStatus(treeView1.Nodes);
BindTreeViewData(); // 綁定TreeView
SetTreeNodesStatus(treeView1.Nodes);

2018年1月17日 星期三

C# Window Form - 動態指定Form尺寸

如果想要動態控制 Form 的尺寸,做法會先指定以某個控制項(或原點)做為基準,指定控制項的位置後,確定最右下角的那個控制項之後,才能決定 Form 的尺寸。

比方右下角有一個 Button(button1),指定的 Form 的語法會是
this.Height = button1.Top + button1.Height;
this.Width = button1.Left + button1.Width;
但這樣加完的結果是看不到,因為 Form 的最上方有一條標題列,要再加上這標題列的高度才行,所以改成
this.Height = button1.Top + button1.Height + iControlSpacer + (this.Height-this.ClientSize.Height);
this.Width = button1.Left + button1.Width + iControlSpacer + (this.Width - this.ClientSize.Width);
其中 this.Height-this.ClientSize.Height 是標題列高度的計算方式,
iControlSpacer  是控制項之間的間隔

2017年9月30日 星期六

Google Cloud Platform 探索經驗 (5) - 將 Google Storage 中的檔案當網站

雖然 Google 有提供部落格 Blogger 可以使用,網路上也有教學將個人部落格改成公司網站使用,但畢竟是部落格的使用方式,在設計上難免會困難度較大。

所以 Google Cloud Platform 有另一個工具 - Storage 可以將寫好的網頁或檔案放在裡面(個人覺得類似於 Google 文件),可以公開連結給需要的人,就變成像是網站的感覺。

接下來就將個人的經驗寫下來供參考

【網站檔案設定】
步驟一:當然要先有 Google Cloud Platform (以下簡稱 GCP)的權限
步驟二:到 GCP 的控制台,展開左邊 ≡ 符號,然後點選 Storage

步驟三:建立 BUKET (此 BUKET 的名稱要和你的網域名稱相同)
步驟四:點選剛才建立 BUKET ,然後將 首頁錯誤頁 的網頁上傳

步驟五:在 BUKET 清單頁找到剛才建立的 BUKET 右邊會有一個 (三個黑點)的符號,點下去之後選擇 "編輯網站設定",將首頁和錯誤頁設好

步驟六:此時點選 BUKET 下的 index.html 的 公開連結,就可以看到網站的首頁了,但是此時網址還是你 Google Storage 的樣子,所以接下來要到你的網域申請的網站上設定DNS對應到的網址。


【網域申請與設定】-這邊是以免費網域申請(https://my.freenom.com/)為範例

步驟一:進入官網後,找到 SERVICE 下的 Register a New Domain,將想要的網域名稱打進去之後,按下 Check Availability,確認看看是否可用和所需費用,選擇清單中想要的那一個,按下 Get it now,然後按下上面的 checkout 後,選擇要用的區間(既然有 12 個月免費可以選,哪有不選的道理),選擇之後按下 Continue,在左下角填入你的 Email Address(這會用來確認並啟用),並按下 Verify My Email Address,然後到你的信箱去點確認連結。

步驟二:點下確認信中的連結後,會要你填寫一些資料(好像全部都是必填),然後勾選I have read and agree to the Terms & Conditions,然後按下 Complete Order,完成申請。

步驟三:完成申請後,要將 Google Storage 和 DNS 做連結,所以找到官網上的 Services 下的 My Domains,選擇剛才申請的 Domain 的 Manage Domain,再點選 Manage Freenom DNS 來設定關聯。


步驟四:先跳到 Google 的 Cloud Storage 文件(Hosting a Static Website)來看一下如何做確認,這邊說明要到你的DNS申請的地方做的設定,但設定前需要先做一下確認,所以先跳到Google 網站網理員中心,網頁告訴你驗證的方法。


步驟五:Google 網站網理員中心 ,按下新增資源,輸入要驗證的網域後按下繼續
,選擇 其他方法 > 網域名稱供應商 > 其他,選擇下方的 新增 CNAME 紀錄
這是要填入步驟三中的設定,供確認用

步驟六:我們將【網域申請與設定】步驟一申請的那個網域填入上列相對資訊,按下 Save Change
然後再回到步驟五的地方按下下方的"驗證",(驗證可能不會馬上生效,需要幾分鐘時間後,才會生效),如果驗證過了,表示你是該網域的擁有者。就會在你的資源清單中看到剛才的資源了


步驟七:開始瀏覽器,在網址列打上你設定的網域,就可以看到和 Google Storage 中一樣的網頁內容了 (如同【網站檔案設定】中的步驟六)


2017年7月25日 星期二

GridView 自訂分頁

一般 GridView 可以交給自動分頁產生出分頁效果,只需要將 GridView 的屬性 AllowPaging 設為 true,再搭配取得資料的語法,即可完成。
GridView 會依據資料筆數 和 GridView.PageCount 自動計算出頁數。
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        GridView1.DataSource = GetData();
        GridView1.DataBind();
    }
}

private DataTable GetData()
{
    // 讀取資料
}

// 翻頁時改變 PageIndex 後重新綁定 GridView 的資料
protected void GridView1_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
    GridView gridview = (GridView)sender;
    GridView.PageIndex = e.NewPageIndex;
    GridView.DataSource = GetData();
    GridView.DataBind();
}
因為GridView其實是將全部的資料都載入後,只顯示某一頁的資訊出來,所以當讀取的資料太多時就會造成載入的間很久。


此時可以使用自訂分頁的方式,自訂分頁除了要將 GridView 的屬性 AllowPaging 設為 true 之外,還要將 GridView 的屬性 AllowCustomPaging 設為 true,然後再在程式碼中指定 GridView.VirtualItemCount (虛擬 GridView 總共有幾筆資料),然後 GridView 會依據 VirtualItemCount 和 GridView.PageCount 自動計算出頁數,具體做法大致如下:
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        // GridView.VirtualItemCount:虛擬 GridView 總共有幾筆資料
        GridView1.VirtualItemCount = GetAllDataCount();
        GridView1.DataSource = GetSpecPageData(0);
        GridView1.DataBind();
    }
}
private int GetAllDataCount()
{
    // 讀取全部資料的筆數
}

/// 只讀取指定頁的資料
private DataTable GetSpecPageData(int PageIndex)
{
    // 比方每頁要顯示 15 筆
    // 讀取第一頁要顯示的資料,那就只讀取 TOP 15
    // 或是若要讀取第三頁 31 到第 45 筆,則可以用 OFFSET 30 ROWS FETCH NEXT 15 ROWS ONLY
    // 意思是是位移 30 筆之後,再往下取 15 筆資料
   請參考MIS2000 Lab
}

翻頁時改變 PageIndex 後重新綁定 GridView 的資料
protected void GridView1_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
    GridView gridview = (GridView)sender;
    GridView.PageIndex = e.NewPageIndex;
    GridView.DataSource = GetSpecPageData(e.NewPageIndex);
    GridView.DataBind();
}
此時遇到的題目是 GridView 的每頁筆數不一致(比方某個欄位要固定只顯示幾種資料),所以除了上述的寫法之外,還要加上一點點修正,如下:

加上一個全域變數,用以儲存每頁的筆數資訊
Dictionary<int, int> dictCountPerPage;
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        dictPerPageCount = GetPerPageCount();
        GridView1.PageCount = dictPerPageCount[0];  // 指定第一頁的筆數
        // GridView.VirtualItemCount:計算虛擬 GridView 總共有幾筆資料(因為每頁筆數不一,所以以要顯示的該頁的筆數當基準,用以計算出虛擬總筆數,好產生頁數(碼)         GridView1.VirtualItemCount = GridView1.PageCount * dictPerPageCount.Count;         GridView1.DataSource = GetSpecPageData(0);         GridView1.DataBind();     } } /// 依需求記錄每頁要顯示的資料筆數 /// Key: 頁碼, Value: 每頁到第幾筆資料 private Dictionary<int, int> GetPerPageCount() {     // 讀取全部每頁的資料的筆數     // 例如:     // Dictionary<int, int> dict = new Dictionary<int, int>();     // dict.Add(0, 10); // 第 1 頁: 0~10 筆     // dict.Add(1, 16); // 第 2 頁: 11~16 筆    // dict.Add(2, 18); // 第 3 頁: 17~18 筆     // dict.Add(3, 22); // 第 4 頁: 19~22 筆     // Session.Add("PerPageCount", dictPerPageCount); 將分頁資訊記錄在 Session     // return dict; }
/// 只讀取指定頁的資料 private DataTable GetSpecPageData(int PageIndex) {     // 比方每頁要顯示 15 筆     // 讀取第一頁要顯示的資料,那就只讀取 TOP 15     // 或是若要讀取第三頁 31 到第 45 筆,則可以用 OFFSET 30 ROWS FETCH NEXT 15 ROWS ONLY     // 意思是是位移 30 筆之後,再往下取 15 筆資料     請參考MIS2000 Lab     // SQL 語法:    StringBuilder SQL = new StringBuilder("...");     SQL.Append(" ORDER BY 1, 2, 3, 4");     // 取回第幾筆~第幾筆資料     if (CurrentPage == 0)         SQL.AppendFormat(" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", 0, dicRowCountPerPage[CurrentPage]);     else         SQL.AppendFormat(" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", dictCountPerPage[PageIndex- 1], dictCountPerPage[PageIndex] - dictCountPerPage[PageIndex- 1]);     // 每次都重新指定 PageCount 和 VirtualItemCount     if (PageIndex== 0)         GridView1.PageSize = dictCountPerPage[PageIndex];     else         GridView1.PageSize = dictCountPerPage[PageIndex] - dictCountPerPage[PageIndex - 1];     GridView1.VirtualItemCount = GridView1.PageSize * dictCountPerPage.Count; } /// 翻頁時改變 PageIndex 後重新綁定 GridView 的資料 protected void GridView1_PageIndexChanging(object sender, GridViewPageEventArgs e) {     GridView gridview = (GridView)sender;     GridView.PageIndex = e.NewPageIndex;     if (dictCountPerPage == null)         dictCountPerPage = (Dictionary<int, int>)Session["PerPageCount"];     GridView.DataSource = GetSpecPageData(e.NewPageIndex);     GridView.DataBind(); }

2017年6月16日 星期五

ASP.NET AJAX 的 UpdateProgress

雖然 VISUAL STUDIO 有提供 UpdateProgress 可以使用,而且也會在更新時出現,更新後就消失,但總覺得顯示的位置不太好控制,所以就乾脆把整個畫面都變色,這樣就可以達到提醒的作用了,方法如下,將 updateprogress 裡面放上想要顯示的字或 gif,然以用 span 將它包起來,然後再用另一個 div,將全部包起來,再用 css  的效果顯示半透明色和調整位置。

Sample.aspx
<style>
.LoadingCover {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    color: white;
    font-size: 20px;
    line-height: 100%;
    background: rgba(0%,0%,0%,0.6);
    z-index: 998;
}

.LoadingContent {
    width: 200px;
    height: 100px;
    position: relative;
    top: 50%;
    left: 50%;
    margin-top: -50px;
    margin-left: -100px;
}
</style>

<asp:ScriptManager ID="ScriptManager1" runat="server" />
<asp:UpdatePanel ID="UpdatePanel1" runat="server" OnLoad="UpdatePanel1_Load">
    <ContentTemplate>
         要更新的內容
    </ContentTemplate>
</asp:UpdatePanel>
<asp:UpdateProgress ID="UpdateProgress1" runat="server" AssociatedUpdatePanelID="UpdatePanel1">
    <ProgressTemplate>
        <div class="LoadingCover">
            <span class="LoadingContent">Loading...</span>
        </div>
    </ProgressTemplate>
</asp:UpdateProgress>      

2017年5月5日 星期五

Google Cloud Platform 探索經驗 (1) - 專案建立

1.當然要先有一個 GOOGLE 帳號,然後登入 Google Cloud Platform 首頁
2.點選網頁左上方"請選取專案"來建立或選擇專案

     如果都沒建立過任一專案,則會出現以下畫面,然後點選畫面右方的"+"來建立新專案
建立新專案 / 選擇專案

輸入專案名稱與專案ID
       專案新專案會需要一段時間,待右上方出現訊息時,表示建立完成
建立專案中

專案建立完成
3.完成後再點擊一次左上角的 ≡ 符號,可看到目前的資訊主頁




Windows 下安裝 MySQL Client

1.到MySQL 官網下載安裝檔
2.安裝過程中選擇 Client Only 選項即可
3.安裝過程中會檢查必要安裝,
      若缺少 Visual Studio Tools For Office 2010,則到微軟官網下載
      若缺少 Python 3.4,則到 Python官網下載(for Windows 版本),或參考這篇文章
       先把 Visual Studio For Office 2010 和 Python 3.4 安裝起來

4.安裝完之後,可以選擇畫面中的單選鈕後,按下下方的"Check"鈕後,即可往下一步安裝
5.按下Execute,後即完成安裝,過程中可能會有些東西要手動按下 "try again",其中 "MySQL for Visual Studio 1.2.7" 會安裝比較久

       全部安裝完成,即可按下下一步
6.這邊會顯示接下來會做哪些東西的設定
 7.MySQL Router 的設定:如果要設定,可以勾選之後,就會可以編輯相關的連線資訊,或者之後再設定,則把勾勾拿掉,直接按下一步