SQLの窓

2016年06月28日


PHP のファイルアップロードで画像ファイルを限定で行う為のテンプレートと注意事項と解説

Webアプリのサンプルソースとして基本的な事

● キャラクタセットは utf-8 で、http ヘッダも meta 要素も統一してどちらも記述します
● イザと言う時の為に、jquery のライブラリは読み込んでいます。これは、デベロッパーツールのコンソールで何かしたい時にとても有効です。
● スマホ表示用として、meta 要素の viewport は記述しておきます。
● デバッグ表示は、print_r( $変数, true ) を pre 要素で挟んで表示します
● コメントを好きな場所に書いて、結果が消えるように <?php // コメントの記述 ?> というような記述方法を使っています。

ブラウザキャッシュについて

PHP の関数として用意されている session_cache_limiter('nocache'); を使用しています。ただ、これを使うには、セッションを開始する必要があります。ここでは、セッションは使用していませんが、いつでも使用できるようにしても損はありません。

ファイルのアップロード処理として2アクションを使用

画面には、アップロードする為の UI に記述を限定しています。実装としてはHTML でも良いのですが、後々の事や、キャッシュやキャラクタセット、そして PHP コメントの使用を考えれば、PHP で記述するほうが得です。但し、機能としてはほぼ、クライアントの処理のみに限定しています。

さらに、IFRAME を使用して、結果によって画面を崩さないようにしています。実用的に考えた場合、この画面自体を IFRAME にして、自由に開いたり閉じたりする UI を想定しています。その場合、3階層のウインドウになりますが、同一ドメインであれば相互の処理は jQuery で容易に行えるので、現在デバッグ情報を表示している最下層の IFRAME で jQuery を出力すれば、フォーム画面の表示調整は容易です。

2アクション目の、upload.php の役目も、ファイルの処理のみに限定でき、メンテナンスや拡張も容易になるはずです。

ファイルアップロード時のエラーコード

PHP のドキュメントを参照して下さい。通常、2(MAX_FILE_SIZE オーバー) か 4 のエラーになると思います

$_FILES 情報の type のウソについて

この情報は正確ではありません。拡張子より決定しているようなので、exif_imagetype で正しいフォーマットを決定して使用しています。Windows 環境では、php.ini で php_exif.dll を有効にする必要があるので注意して下さい。

日本語ファイル名について

日本語部分を urlencode する事によって、一般的に WEB環境のディスクに保存できるようになります。但し、その文字のまま、WEB で呼び出そうとすると Not Found となるので注意して下さい。ブラウザ等から呼び出す場合、urlencode 部分が元に戻ってサーバに渡されます。なので、WEBアプリのクライアント側で扱う文字列としては、もう一度 urlencode しておく必要があります。

同一ファイルのアップロードについて

簡単に uniqid() の結果を頭に付けてファイル名を決定しています。ブラウザ側で、ギャラリーとして表示する場合は、uniqid() 部分を取り除いて表示する事によってオリジナルファイル名となります。しかし、ここでもしそのファイルが日本語の場合、urlencode されているので、urldecode する必要がありますが、そのままでは Windows 環境からアップロードされたものは、SHIFT_JIS のままだという事に注意して下さい。ギャラリーのページのキャラクタセットが SHIFT_JIS 以外の場合は、mb_convert_encoding で変換が必要になります

未実装の処理について

このままでは、ファイルがアップロードされて images フォルダに増えて行くばかりとなります。画像という前提の為、『ギャラリー』を作成する必要がありますが、画像の表示には、メモリを圧迫しない小さな画像ファイル(サムネイル)が必要になります。その為には、GD を使用してアップロードが成功した後に、縮小したファイルを作成する必要があります。

サムネイルが作成されれば、一覧表示をファイルシステム関数を使用して表示するだけですが、オリジナル画像を参照する為に、ポピューラーな Lightbox2 ライブラリ等を実装して使用する事となります。

アップロードフォーム
▼ file_upload.php
<?php
// *************************************
// キャラクタセット
// *************************************
header( "Content-Type: text/html; charset=utf-8" );
// *************************************
// キャッシュ無効
// *************************************
session_cache_limiter('nocache');
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<?php // キャラクタセット ?>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<?php // スマホ用 ?>
<meta name="viewport" content="width=device-width,initial-scale=1.0">

<?php // イザと言う時の為 ?>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>

<style>
body {
	margin: 0;
	padding: 30px;
	background-color: #d0d0d0;
}
body,pre,input {
	font-size: 16px;

<?php // よくある(ポピュラーな)設定 ?>
	font-family: "ヒラギノ角ゴPro W3","Hiragino Kaku Gothic Pro","メイリオ",Meiryo,"MS Pゴシック",Verdana,Arial,Helvetica,sans-serif;
}
</style>

</head>

<body>
<form
<?php // ファイルアップロードで必須 ?>
	enctype="multipart/form-data"
	method="post"

<?php // ファイルがアップロードされる php ?>
	action="upload.php"

<?php // 結果が表示される IFRAME の名称 ?>
	target="result_window">

<?php // php の画像サイズ制限の設定 ?>
	<input type="hidden" name="MAX_FILE_SIZE" value="1000000">

<?php // ローカルファイル参照用 ?>
	<input type="file" id="file" name="file" size="40">
	<br>
	<br>
	<input
		type="submit"
		name="send"
		value="開始">
	<br>
	<br>
</form>

<?php // 結果が表示される IFRAME ?>
<iframe
	name="result_window"
	frameborder="1"
	scrolling="yes"
	width="100%"
	height="400"
	style='background-color:white;'
></iframe>


</body>
</html>


アップロードされたファイルの処理
▼ upload.php
<?php
// *************************************
// キャラクタセット
// *************************************
header( "Content-Type: text/html; charset=utf-8" );
// *************************************
// キャッシュ無効
// *************************************
session_cache_limiter('nocache');
session_start();

// ファイルを移動するフォルダ
$target_folder = "./images/";

// *************************************
// アップロードされたファイル
// *************************************
$ok = ( $_FILES['file']['error'] == 0 );
if ( $ok ) {

	// *************************************
	// 以降、アップロードそのものは成功
	// *************************************

	// *************************************
	// 1) 画像フォーマットの取得
	// *************************************
	$type_string = image_type_to_mime_type( exif_imagetype( $_FILES['file']['tmp_name'] ) );

	// *************************************
	// 2) オリジナルファイル名の取得
	// *************************************
	$file = explode(".", $_FILES['file']['name']);

	// *************************************
	// 3) 日本語ファイル名対応
	// *************************************
	$file_name = urlencode( $file[0] );

	// *************************************
	// 4) 保存ファイル名を作成
	//   a) 拡張子決定
	//   b) uniqid() でファイル目をユニーク
	// *************************************
	$target = "";
	if ( $type_string == "image/jpeg" ) {
		$target = uniqid() . "_{$file_name}.jpg";
	}
	if ( $type_string == "image/gif" ) {
		$target = uniqid() . "_{$file_name}.gif";
	}
	if ( $type_string == "image/png" ) {
		$target = uniqid() . "_{$file_name}.png";
	}
	if ( $target == "" ) {
		$result = "アップロードできないフォーマットです\n";
	}
	else {
		// *************************************
		// アップロードファイルの保存
		// *************************************
		if ( @move_uploaded_file( $_FILES['file']['tmp_name'], $target_folder . $target ) ) {
			$result = "アップロードに成功しました\n";
		}
		else {
			// なんらかの環境エラー
			$result = "アップロードに失敗しました\n";
		}
		
	}
}
else {
	$result = "ファイルはアップロードされていません";
}
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>

<style>
body {
	margin: 0;
	padding: 40px;
	background-color: #ffffff;
}
body,pre {
	font-size: 16px;
	font-family: "ヒラギノ角ゴPro W3","Hiragino Kaku Gothic Pro","メイリオ",Meiryo,"MS Pゴシック",Verdana,Arial,Helvetica,sans-serif;
}
</style>

</head>

<body>
<h3><?= $result ?></h3>

<?php // 情報を最も簡単に表示する方法 ?>
<pre>
<?= print_r( $_FILES, true ) ?>
<?= print_r( $_POST, true ) ?>
</pre>
<?php // urlencode は、日本語のファイル名に必要です ?>
<img src="<?= $target_folder . urlencode( $target ) ?>" style='width:90%;'>
</body>
</html>


オリジナルファイル名については、. がファイル名に含まれないように考慮しています。


posted by lightbox at 2016-06-28 18:06 | PHP + 特記事項 | このブログの読者になる | 更新情報をチェックする

2016年06月26日


カスタム・リストビュー・ダイアログ : DialogFragment 内の ListView を ArrayAdapter でカスタムする



結果的には、DialogFragment を使用しない場合より簡単に実装できました。正直これで正しいのかどうかは解りませんが、ダイアログのインスタンスはシステムで再作成される事を前提にしているので、DialogFragment 内の onCreateDialog で全て実装する事によって、再度 View をセットするというような処理は必要ありません。但し、行を選択して閉じる時に、dismiss() によってダイアログを終わらせています。

リストビューのデータは、本来 newInstance で put すべきでしょうが、onCreateDialog 内で全て完結するほうがスッキリしているのでそうしました。

行のクリックでは、View からデータを取得していますが、MyData に何かメソッドを実装して実行したい場合は、
MyAdapter adapter = (MyAdapter) parent.getAdapter();
MyData my_data = (MyData) adapter.getItem(position);
で問題無いと思います MainActivity 内のソースコード
@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);

	// *********************************
	// ボタンをクリック
	// ( ダイアログフラグメント で表示 )
	// *********************************
	MainActivity.this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
		@Override
		public void onClick(View v) {

			 MyDialogFragment my_dialog_fragment = MyDialogFragment.newInstance("地域一覧");

			// ダイアログの表示
			// ※ この環境では、getSupportFragmentManager が必要です
			my_dialog_fragment.show(getSupportFragmentManager(),"dialog");

		}
	});

}

// *********************************
// static クラスの定義
// 1) DialogFragment
// 2) ArrayAdapter
// 3) MyData
// *********************************
public static class MyDialogFragment extends DialogFragment {

	// ユーザが呼び出す為のもの( 外側で処理しても良い )
	public static MyDialogFragment newInstance(String title) {
		// インスタンス作成
		MyDialogFragment my_dialog_fragment = new MyDialogFragment();
		// システムに引数を保存
		Bundle args = new Bundle();
		args.putString("title", title);
		my_dialog_fragment.setArguments(args);
		return my_dialog_fragment;
	}

	// *********************************
	// ここが、ダイアログの作成時に呼ばれます
	// 初回はユーザがダイアログを作成
	// デバイスを横にした時はシステムが
	// ダイアログを作成します
	// *********************************
	@Override
	public Dialog onCreateDialog(Bundle savedInstanceState) {

		// この中で使用するダイアログ作成用
		final AlertDialog.Builder ad_builder_in_fragment;

		// この DialogFragment に保存されているデータを取得
		// ( システムが再作成した場合でも Bundle よりデータを取得できます )
		String title = MyDialogFragment.this.getArguments().getString("title");

		// DialogFragment から Activity を取得して使う
		// ( ダイアログが使われる Activity を MyDialogFragment 内で取得しています )
		ad_builder_in_fragment = new AlertDialog.Builder(MyDialogFragment.this.getActivity());
		// タイトル設定
		ad_builder_in_fragment.setTitle(title);
		ad_builder_in_fragment.setNegativeButton("キャンセル", new DialogInterface.OnClickListener() {
			@Override
			public void onClick(DialogInterface dialog, int which) {
				// キャンセル
			}
		});
		// *********************************
		// ダイアログのメインビューに
		// リストビューを設定
		// *********************************
		LayoutInflater inflater = (LayoutInflater) MyDialogFragment.this.getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
		ListView list_view = (ListView) inflater.inflate(R.layout.listview, null);
		// *********************************
		// 行をクリックした時のイベント
		// *********************************
		list_view.setOnItemClickListener(new AdapterView.OnItemClickListener() {
			@Override
			public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

				// View から行データを取得
				TextView tv;
				tv = (TextView) view.findViewById(R.id.textPlace);
				String textPlace = tv.getText().toString();
				tv = (TextView) view.findViewById(R.id.textCode);
				String textCode = tv.getText().toString();

				// メッセージの作成
				String message = String.format("%s : %s", textPlace, textCode);
				// logcat に表示
				Log.i("lightbox", message );
				// Toast で表示
				Toast.makeText(MyDialogFragment.this.getActivity(),message, Toast.LENGTH_SHORT).show();
				// TextView に表示
				tv = (TextView) MyDialogFragment.this.getActivity().findViewById(R.id.textView);
				tv.setText(message);

				// 破棄
				MyDialogFragment.this.dismiss();
			}
		});
		// *********************************
		// カスタム・アダプター
		// *********************************
		MyAdapter adapter = new MyAdapter(MyDialogFragment.this.getActivity(),R.layout.listview_item);

		// *********************************
		// ListView に表示するデータ
		// *********************************
		List<MyData> list = new ArrayList<MyData>();
		list.add(new MyData("大阪","27"));
		list.add(new MyData("東京","13"));
		list.add(new MyData("岡山","33"));
		list.add(new MyData("北海道","1"));
		list.add(new MyData("沖縄","47"));
		list.add(new MyData("京都","26"));
		list.add(new MyData("滋賀","25"));

		// *********************************
		// 新しく領域確保して配列データを作成する
		// *********************************
		MyData[] my_data = list.toArray( new MyData[0] );

		// *********************************
		// カスタム・アダプターにデータをセット
		// *********************************
		adapter.addAll(my_data);
		// *********************************
		// ListView にカスタム・アダプターをセット
		// *********************************
		list_view.setAdapter(adapter);

		ad_builder_in_fragment.setView(list_view);

		// ダイアログを作成してシステムに返す
		return ad_builder_in_fragment.create();
	}
}

// *********************************
// MyData 専用アダブター
// *********************************
public static class MyAdapter extends ArrayAdapter {

	public MyAdapter(Context context, int resource) {
		super(context, resource);
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {

		View rowView = convertView;
		if (rowView == null) {
			// 現在の View の取得
			LayoutInflater inflater = (LayoutInflater) getContext().getSystemService
				(Context.LAYOUT_INFLATER_SERVICE);
			rowView = inflater.inflate(R.layout.listview_item, null);
		}

		MyData my_data = (MyData)this.getItem(position);

		TextView tv;
		// listview_item 内の textPlace にデータをセット
		tv = (TextView) rowView.findViewById(R.id.textPlace);
		tv.setText(my_data.toString());
		// listview_item 内の textCode にデータをセット
		tv = (TextView) rowView.findViewById(R.id.textCode);
		tv.setText(my_data.getValue());

		return rowView;
	}
}

// *********************************
// MyData
// *********************************
public static class MyData {

	private String myString;
	private String myValue;

	public MyData(String myString,String myValue) {
		this.myString = myString;
		this.myValue = myValue;
	}

	// データ取得用
	String getValue() {
		return myValue;
	}

	@Override
	public String toString() {
		return myString;
	}
}


※ 全てのソースコードはこちらにあります




posted by lightbox at 2016-06-26 01:33 | 2 Android Studio | このブログの読者になる | 更新情報をチェックする

カスタム・リストビュー・ダイアログ : ダイアログ内の ListView を ArrayAdapter でカスタムする


※ この処理には、DialogFragment は使用していません。

AlertDialog 内で表示されるリストビューを外側で定義してカスタマイズするサンプルです。AlertDialog.Builder の setView で、自由にダイアログ内のメインコンテンツを差し替えれるので、ListView をそのままセットしてみました。

もちろん、ArrayAdapter を継承した独自クラスを作成して、お決まりのその中での getView の処理も実装します。問題は、表示される度に ListView をセットしなおす必要がある事をテストして知りましたが、ListView を setView でセットした後に再度 setView する場合、いったん ListView を ダイアログ内の View から削除する必要がありました。

通常、DialogFragment では無い使い方では、 AlertDialog.Builder で作成された AlertDialog は常にメモリに残っているのでそのような処理が必要なのだと思います。ボタンがクリックされる度に、AlertDialog を 毎回 1 から作成して、使うのであればそのような必要も無いのでしょうが、そうなると使用後に削除する必要が出て来るのでこのような実装になっています。

行のクリックは、ListView の OnItemClickListener で行う事になるので、AlertDialog の hide メソッドで閉じています。

また、行データの参照は、Adapter を直接参照できるので、Adapter の getItem 経由で行っています。

MainActivity 内のソースコード
private AlertDialog ad = null;
private AlertDialog.Builder ad_builder;
private MyData[] my_data;
private ListView list_view;
private MyAdapter adapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);

	// *********************************
	// ListView に表示するデータ
	// *********************************
	List<MyData> list = new ArrayList<MyData>();
	list.add(new MyData("大阪","27"));
	list.add(new MyData("東京","13"));
	list.add(new MyData("岡山","33"));
	list.add(new MyData("北海道","1"));
	list.add(new MyData("沖縄","47"));
	list.add(new MyData("京都","26"));
	list.add(new MyData("滋賀","25"));

	// *********************************
	// 新しく領域確保して配列データを作成する
	// *********************************
	my_data = list.toArray( new MyData[0] );

	// *********************************
	// ノーマルダイアログ作成用インスタンス作成
	// *********************************
	ad_builder = new AlertDialog.Builder(MainActivity.this);
	// *********************************
	// タイトル設定
	// *********************************
	ad_builder.setTitle("地域一覧");
	// *********************************
	// キャンセルイベント
	// *********************************
	ad_builder.setNegativeButton("キャンセル", new DialogInterface.OnClickListener() {
		@Override
		public void onClick(DialogInterface dialog, int which) {
			// キャンセル
		}
	});
	// *********************************
	// ダイアログのメインビューに
	// リストビューを設定
	// *********************************
	LayoutInflater inflater = (LayoutInflater) MainActivity.this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	list_view = (ListView) inflater.inflate(R.layout.listview, null);
	// *********************************
	// 行をクリックした時のイベント
	// *********************************
	list_view.setOnItemClickListener(new AdapterView.OnItemClickListener() {
		@Override
		public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

			// Adapter 内の行データを取得
			MyData my_data = (MyData) adapter.getItem(position);

			// メッセージの作成
			String message = String.format("%s : %s", my_data.toString(), my_data.getValue());
			// logcat に表示
			Log.i("lightbox", message );
			// Toast で表示
			Toast.makeText(MainActivity.this,message,Toast.LENGTH_SHORT).show();
			// TextView に表示
			TextView tv;
			tv = (TextView) MainActivity.this.findViewById(R.id.textView);
			tv.setText(message);

			// 非表示
			ad.hide();
		}
	});
	// *********************************
	// カスタム・アダプター
	// *********************************
	adapter = new MyAdapter(MainActivity.this,R.layout.listview_item);
	// *********************************
	// カスタム・アダプターにデータをセット
	// *********************************
	adapter.addAll(my_data);
	// *********************************
	// ListView にカスタム・アダプターをセット
	// *********************************
	list_view.setAdapter(adapter);

	// *********************************
	// ボタンをクリック
	// *********************************
	MainActivity.this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
		@Override
		public void onClick(View v) {

			// 初回はなにもしない
			if ( ad != null ) {
				// ダイアログ内の親 View から ListView を削除
				ViewGroup vg = (ViewGroup) list_view.getParent();
				vg.removeView(list_view);
			}
			// ダイアログに ListView をセット
			ad_builder.setView(list_view);
			// ダイアログの表示
			// ( AlertDialog は 非表示処理に使用 )
			ad = ad_builder.show();

		}
	});

}

// *********************************
// MyData 専用アダブター
// *********************************
private class MyAdapter extends ArrayAdapter {

	public MyAdapter(Context context, int resource) {
		super(context, resource);
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {

		View rowView = convertView;
		if (rowView == null) {
			// 現在の View の取得
			LayoutInflater inflater = (LayoutInflater) MainActivity.this.getSystemService
				(Context.LAYOUT_INFLATER_SERVICE);
			rowView = inflater.inflate(R.layout.listview_item, null);
		}

		MyData my_data = (MyData)this.getItem(position);

		TextView tv;
		// listview_item 内の textPlace にデータをセット
		tv = (TextView) rowView.findViewById(R.id.textPlace);
		tv.setText(my_data.toString());
		// listview_item 内の textCode にデータをセット
		tv = (TextView) rowView.findViewById(R.id.textCode);
		tv.setText(my_data.getValue());

		return rowView;
	}
}

// *********************************
// MyData
// *********************************
private class MyData {

	private String myString;
	private String myValue;

	public MyData(String myString,String myValue) {
		this.myString = myString;
		this.myValue = myValue;
	}

	// データ取得用
	String getValue() {
		return myValue;
	}

	@Override
	public String toString() {
		return myString;
	}
}


※ 全てのソースコードはこちらにあります



posted by lightbox at 2016-06-26 01:15 | 2 Android Studio | このブログの読者になる | 更新情報をチェックする
Seesaa の各ページの表示について
Seesaa の 記事がたまに全く表示されない場合があります。その場合は、設定> 詳細設定> ブログ設定 で 最新の情報に更新の『実行ボタン』で記事やアーカイブが最新にビルドされます。

Seesaa のページで、アーカイブとタグページは要注意です。タグページはコンテンツが全く無い状態になりますし、アーカイブページも歯抜けページはコンテンツが存在しないのにページが表示されてしまいます。

また、カテゴリページもそういう意味では完全ではありません。『カテゴリID-番号』というフォーマットで表示されるページですが、実際存在するより大きな番号でも表示されてしまいます。

※ インデックスページのみ、実際の記事数を超えたページを指定しても最後のページが表示されるようです

対処としては、このようなヘルプ的な情報を固定でページの最後に表示するようにするといいでしょう。具体的には、メインの記事コンテンツの下に『自由形式』を追加し、アーカイブとカテゴリページでのみ表示するように設定し、コンテンツを用意するといいと思います。


※ エキスパートモードで表示しています

アーカイブとカテゴリページはこのように簡単に設定できますが、タグページは HTML 設定を直接変更して、以下の『タグページでのみ表示される内容』の記述方法で設定する必要があります

<% if:page_name eq 'archive' -%>
アーカイブページでのみ表示される内容
<% /if %>

<% if:page_name eq 'category' -%>
カテゴリページでのみ表示される内容
<% /if %>

<% if:page_name eq 'tag' -%>
タグページでのみ表示される内容
<% /if %>
この記述は、以下の場所で使用します
container 終わり

フリーフォントで簡単ロゴ作成
フリーフォントでボタン素材作成
フリーフォントで吹き出し画像作成
フリーフォントではんこ画像作成
ほぼ自由に利用できるフリーフォント
フリーフォントの書体見本とサンプル
画像を大きく見る為のウインドウを開くボタンの作成

CSS ドロップシャドウの参考デモ
BUTTONS (CSS でボタン)
イラストAC
ぱくたそ
写真素材 足成
フリーフォント一覧
utf8 文字ツール
右サイド 終わり
base 終わり