Журнал LinuxFormat - перейти на главную

LXF164: Android

Материал из Linuxformat
Версия от 16:18, 26 октября 2018; Olkol (обсуждение | вклад)

(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск


Android

Программирование: Взаимодействие с web-камерой телефона


Содержание

Де­ла­ем сним­ки

Не нра­вит­ся про­грам­ма для ка­ме­ры в сво­ем те­ле­фоне? Есть идея про­грам­мы для ра­бо­ты с фо­то? Джуль­ет­та Кемп зна­ко­мит с Android Camera API.

(thumbnail)
Наш эксперт Джуль­ет­та Кемп еще пом­нит вре­ме­на, ко­гда в фо­то­ап­па­ра­тах бы­ла на­стоя­щая плен­ка. Сей­час она де­ла­ет гораздо поболее сним­ков, чем то­гда.

В по­дав­ляю­щем боль­шин­ст­ве те­ле­фо­нов с Android есть встро­ен­ные ка­ме­ры, и с раз­ви­ти­ем тех­но­ло­гий они ста­но­вят­ся все удобнее. Про­грамм, ко­то­рые так или ина­че ра­бо­та­ют с ка­ме­рой, ве­ли­кое мно­же­ст­во, и пре­достав­ляе­мое Android API мак­си­маль­но уп­ро­ща­ет эту за­да­чу.

Ес­ли вам ну­жен все­го-то один снимок, воз­мож­но, про­ще восполь­зо­вать­ся На­ме­рением [Intent] для ак­ти­ва­ции встро­ен­ной ка­ме­ры (см. врез­ку внизу), но ес­ли вам нуж­но боль­ше ры­ча­гов управ­ления, мож­но на­пи­сать соб­ст­вен­ное За­ня­тие [Activity] Camera.

На на­шем уро­ке мы соз­да­дим За­ня­тие, ко­то­рое про­сто ис­поль­зу­ет ка­ме­ру, но, ра­зу­ме­ет­ся, ничто не ме­ша­ет соз­дать при­ло­жение с дру­гим глав­ным За­ня­ти­ем, где за­од­но бу­дет и За­ня­тие для ка­ме­ры, вы­зы­вае­мое при необ­хо­ди­мо­сти.

Под­го­тов­ка ка­ме­ры

Пре­ж­де чем пи­сать код, сле­ду­ет объ­я­вить тре­бо­ва­ния для ка­ме­ры в AndroidManifest.xml:

<manifest .... >

<uses-permission android:name=”android.permission.CAMERA” />

<uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE” />

<uses-feature android:name=”android.hardware.camera” />

Ес­ли вы не со­би­рае­тесь со­хра­нять изо­бра­жения, WRITE_EXTERNAL_STORAGE не по­на­до­бит­ся (но так как мы поз­же это сде­ла­ем, то до­ба­вим ее сра­зу). Ди­рек­ти­ва uses-feature оз­на­ча­ет, что для уста­нов­ки при­ло­жения в те­ле­фоне обя­за­тель­но долж­на быть ка­ме­ра. Ес­ли ка­ме­ра – толь­ко часть ва­шей про­грам­мы, и поль­зо­ва­те­ли пре­крас­но смо­гут ра­бо­тать с про­грам­мой и без нее, до­бавь­те android:required=“false” к этой стро­ке. Так­же мож­но за­дать ха­рак­те­ри­сти­ки ка­ме­ры; бо­лее под­роб­но они опи­са­ны в од­ном из сле­дую­щих раз­де­лов.

Те­перь мож­но про­дол­жить с ко­дом пер­во­на­чаль­ной на­строй­ки, в ко­то­ром мы впер­вые по­лу­ча­ем эк­зем­п­ляр объ­ек­та ка­ме­ры. Ме­тод onCreate() в MyCameraActivity дол­жен вы­гля­деть так:

@Override public void onCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);

if (!checkCameraExists(this)) {

Toast.makeText(this, “Увы, ка­ме­ры нет!”, Toast.LENGTH_LONG);

finish();

}

camera = getCameraInstance();

}

Один из па­ра­мет­ров ма­ни­фе­ста тре­бу­ет обя­за­тель­но­го на­ли­чия ка­ме­ры на уст­рой­ст­ве, но ее на­ли­чие сто­ит про­ве­рить и здесь. Ме­тод checkCameraExists() прост:

private boolean checkCameraExists(Context c) {

if (c.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {

return true;

} else {

return false;

}

}

Ме­тод getCameraInstance() не­на­мно­го слож­нее:

private Camera getCameraInstance() {

Camera c = null;

try {

c = Camera.open();

} catch (Exception e) {

Toast.makeText(this, “Увы, ка­ме­ры нет!”, Toast.LENGTH_LONG);

Log.e(TAG, “Нет ка­ме­ры: ис­клю­че­ние “ + e.getMessage());

e.getStackTrace();

finish();

}

return c;

}

Ме­тод Camera.open() об­ра­ща­ет­ся к пер­вой ка­ме­ре на задней по­верх­но­сти уст­рой­ст­ва.

Ка­мер на уст­рой­ст­вах Android мо­жет быть несколь­ко; для досту­па к кон­крет­ной ка­ме­ре восполь­зуй­тесь ме­то­дом Camera.open(int cameraId). Ме­тод Camera.getNumberOfCameras() вернет ко­ли­че­­ст­во ка­мер уст­рой­ст­ва, а ме­тод Camera. getCameraInfo() – ин­фор­ма­цию о за­дан­ной ка­ме­ре. Для боль­шин­ст­ва за­дач нам по­дой­дет пер­вая ка­ме­ра на задней по­верх­но­сти уст­рой­ст­ва.

В сле­дую­щих раз­де­лах мы зай­мем­ся про­смот­ром изо­бра­жения с ка­ме­ры и соз­данием сним­ка. Но пе­ред этим нуж­но сде­лать еще од­ну важ­ную вещь: за­кон­чив ра­бо­ту с ка­ме­рой, осво­бо­ди­те ее.

Ина­че дру­гие про­цес­сы не смо­гут по­лу­чить доступ к ка­ме­ре – это пло­хая прак­ти­ка; кро­ме то­го, это обес­по­ко­ит поль­зо­ва­те­лей.


Внесем нуж­ные из­менения в ме­тод onPause():

@Override protected void onPause() {

super.onPause();

releaseCamera();

}

private void releaseCamera() {

if (camera != null) {

camera.release();

camera = null;

}

}

@Override protected void onResume() {

if (camera == null) {

camera.getCameraInstance();

}

super.onResume();

}

Когда мы соз­дадим ко­д про­смот­ра изо­бра­жения с ка­ме­ры, данный ме­тод при­дет­ся несколько усложнить, но мы зай­мем­ся этим по­том. А по­ка вам важнее все­го помнить, что следует обязательно осво­бо­ждать ка­ме­ру, когда наше при­ло­жение приоста­нав­ли­ва­ет­ся.


Про­смотр изо­бра­же­ния с ка­ме­ры

С по­мо­щью Camera API мож­но сде­лать сни­мок так, что поль­зо­ва­тель ни о чем не уз­на­ет, но в по­дав­ляю­щем боль­шин­ст­ве слу­ча­ев сна­ча­ла нуж­но по­ка­зать изо­бра­же­ние с ка­ме­ры, а за­тем поль­зо­ва­тель на­жмет на кноп­ку. Для это­го соз­да­дим соб­ст­вен­ный класс CameraPreview, унас­ле­до­ван­ный от SurfaceView:

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {

private static final String TAG = “CameraPreview”;

private SurfaceHolder sh;

private Camera camera;

public CameraPreview(Context context, Camera cm) {

super(context);

camera = cm;

sh = getHolder();

sh.addCallback(this);

// ус­та­рев­ший, но тре­буе­мый pre-3.0

sh.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

}

public void surfaceCreated(SurfaceHolder holder) {

try {

camera.setPreviewDisplay(holder);

camera.startPreview();

} catch (IOException e) {

Log.e(TAG, “Ошиб­ка ус­та­нов­ки пред­про­смот­ра: “ + e.getMessage());

e.getStackTrace();

}

}

public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

if (sh.getSurface() == null) {

// Нет по­верх­но­сти пред­про­смот­ра!

return;

}

// Ос­та­нов­ка пред­про­смот­ра до вне­се­ния из­ме­не­ний.

try {

camera.stopPreview();

} catch (Exception e) {

// По­пыт­ка пре­сечь не­су­ще­ст­вую­щий пред­про­смотр

}

try {

camera.setPreviewDisplay(sh);

camera.startPreview();

} catch (Exception e) {

Log.e(TAG, “Ошиб­ка при рес­тар­те пред­про­смот­ра: ” + e.getMessage());

e.getStackTrace();

}

}

public void surfaceDestroyed(SurfaceHolder holder) {

// За­ня­тие сле­дит за ос­во­бо­ж­де­ни­ем пред­про­смот­ра ка­ме­ры

}

}

Этот класс унас­ле­до­ван от SurfaceView, осу­ще­ст­в­ляю­ще­го рен­де­ринг го­раз­до бы­ст­рее, чем стан­дарт­ный View (и от­ри­сов­ка мо­жет вы­пол­нять­ся фо­но­вы­ми нитя­ми, че­го не до­пуска­ет View), что иде­аль­но для бы­ст­рой от­ри­сов­ки, ко­то­рая необходима нам для про­смот­ра изо­бра­жения с ка­ме­ры.

Для досту­па к по­верх­но­сти изо­бра­жения нам по­на­до­бит­ся так­же реа­ли­зо­вать ин­тер­фейс SurfaceHolder.CallBack. Здесь необ­хо­ди­мы ме­то­ды surfaceCreated(), SurfaceChanged() и surfaceDestroyed().

Мы по­лу­ча­ем SurfaceHolder (с по­мо­щью ме­то­да ро­ди­тель­ско­го клас­са), за­тем до­ба­вим к ней Callback, ко­то­рый про­ин­фор­ми­ру­ет кли­ен­та о лю­бых из­менениях с по­верх­но­стью.

На­ша вер­сия surfaceChanged() ниче­го не ме­ня­ет. Же­лая вве­сти из­менения, де­лай­те это ме­ж­ду оста­нов­кой и за­пуском пред­про­смот­ра. Од­на из си­туа­ций, когда это мо­жет при­го­дить­ся – об­ра­бот­ка из­менения по­ло­жения уст­рой­ст­ва (ка­ме­ры и эк­ра­на) в про­стран­ст­ве.

В уст­рой­ст­вах с Android вер­сии до 2.2 пред­про­смотр ав­то­ма­ти­че­­ски вклю­чал­ся как Пей­заж, и для мак­си­маль­ной со­вмес­ти­мо­сти про­ще все­го при­ну­ди­тель­но уста­но­вить ре­жим Пей­за­жа, до­ба­вив ат­ри­бут к эле­мен­ту activity в AndroidManifest.xml:

<activity [ ... ] android:screenOrientation=”landscape” >

Од­на­ко в вер­си­ях с 2.2 и вы­ше это мож­но уб­рать, вза­мен за­дав по­ло­же­ние про­смот­ра со­глас­но по­ло­же­нию уст­рой­ст­ва. До­бавь­те та­кие стро­ки в ме­тод surfaceChanged() в CameraPreview.java ме­ж­ду вы­зо­ва­ми stopPreview() и startPreview():

if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {

camera.setDisplayOrientation(90);

}

Не за­будь­те уда­лить на­строй­ку пей­за­жа из AndroidManifest.xml, и вы уви­ди­те, что пред­про­смотр по­во­ра­чи­ва­ет­ся вслед за по­во­ро­том ка­ме­ры.

Соз­дав класс CameraPreview, до­бавь­те ме­тод setUpLayout(), вы­зы­вае­мый из ме­то­да onCreate() в MyCameraActivity.

Здесь он вы­де­лен в от­дель­ный ме­тод, что­бы про­ще при­ос­та­нав­ли­вать/во­зоб­нов­лять про­грам­му; под­роб­но­сти см. ни­же.

private void setUpLayout() {

setContentView(R.layout.main);

preview = new CameraPreview(this, camera);

FrameLayout frame = (FrameLayout) findViewById(R.id.camera_preview);

frame.addView(preview);

}

Так­же нуж­но до­ба­вить кое-что в файл main.xml:

<?xml version=”1.0” encoding=”utf-8”?>

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”

android:orientation=”horizontal”

android:layout_width=”fill_parent”

android:layout_height=”fill_parent”

>

<FrameLayout

android:id=”@+id/camera_preview”

android:layout_width=”fill_parent”

android:layout_height=”fill_parent”

android:layout_weight=”1”

/>

<Button

android:id=”@+id/button_capture”

android:text=”Capture”

android:layout_width=”wrap_content”

android:layout_height=”wrap_content”

android:layout_gravity=”center”

/>

</LinearLayout>

Ко­дом кноп­ки мы зай­мем­ся в сле­дую­щем раз­де­ле. Ском­пи­ли­руй­те и за­пус­ти­те про­грам­му, и на эк­ра­не долж­но поя­вить­ся изо­бра­же­ние с ка­ме­ры с кноп­кой Capture [Сде­лать сни­мок] ря­дом, хо­тя и при на­жа­тии кноп­ки по­ка ни­че­го не про­изой­дет.

На­ко­нец, ра­нее я го­во­ри­ла, что по­сле на­пи­са­ния ко­да про­смот­ра ме­то­ды onPause() и onResume() нуж­но не­мно­го ус­лож­нить. Ес­ли про­сто ос­во­бо­дить ка­ме­ру, то при во­зоб­нов­ле­нии при­ло­же­ния ста­рый класс пред­про­смот­ра то­же по­про­бу­ет об­ра­тить­ся к объ­ек­ту ка­ме­ры, ко­то­рый у не­го был (и ко­то­рый те­перь ос­во­бо­ж­ден), и вы­зо­вет ис­клю­че­ние.

Что­бы это ис­пра­вить, нуж­но ос­во­бо­ж­дать объ­ект при при­ос­та­нов­ке при­ло­же­ния, а при во­зоб­нов­ле­нии – соз­да­вать но­вый:

protected void onPause() {

releaseCamera();

super.onPause();

}

private void releaseCamera() {

if (camera != null) {

camera.stopPreview();

camera.release();

camera = null;

preview = null;

}

}

protected void onResume() {

if (camera == null) {

camera = getCameraInstance();

setUpLayout();

}

super.onResume();

}

Те­перь вы смо­же­те вый­ти из про­грам­мы и за­тем пе­ре­за­пус­тить ее без оши­бок.

Соз­да­ние сним­ка

У нас есть кноп­ка Capture; те­перь нуж­но свя­зать с ней ка­кие-ни­будь дей­ст­вия. До­бавь­те сле­дую­щий код в setUpLayout():

Button captureButton = (Button) findViewById(R.id.button_capture); captureButton.setOnClickListener(

new View.OnClickListener() {

public void onClick(View v) {

getImage();

}

}

);

Ме­тод getImage() вы­гля­дит так:

protected static final int MEDIA_TYPE_IMAGE = 0;

protected static final int MEDIA_TYPE_VIDEO = 1;

private void getImage() {

PictureCallback picture = new PictureCallback() {

public void onPictureTaken(byte[] data, Camera cam) {

File picFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);

if (picFile == null) {

Log.e(TAG, “Ошиб­ка при соз­да­нии ме­диа-фай­ла; вер­ны ли раз­ре­ше­ния на за­пись?”);

return;

}

try {

FileOutputStream fos = new FileOutputStream (picFile);

fos.write(data);

fos.close();

} catch (FileNotFoundException e) {

Log.e(TAG, “Файл не най­ден: “ + e.getMessage());

e.getStackTrace();

} catch (IOException e) {

Log.e(TAG, “Ошиб­ка I/O фай­ла: “ + e.getMessage());

e.getStackTrace();

}

}

};

camera.takePicture(null, null, picture);

}

private File getOutputMediaFile(int type) {

File directory = new File(Environment.getExternalStorage PublicDirectory(Environment.DIRECTORY_PICTURES), getPackageName());

if (!directory.exists()) {

if (!directory.mkdirs()) {

Log.e(TAG, “Не уда­лось соз­дать ка­та­лог для хра­не­ния.”);

return null;

}

}

String timeStamp = new SimpleDateFormat(“yyyMMdd_HHmmss”).format(new Date());

File file;

if (type == MEDIA_TYPE_IMAGE) {

file = new File(directory.getPath() + File.separator + “IMG_” + timeStamp + “.jpg”);

} else if (type == MEDIA_TYPE_VIDEO) {

file = new File(directory.getPath() + File.separator + “VID_” + timeStamp + “.mp4”);

} else {

return null;

}

return file;

}

В ме­то­де getOutputMediaFile() мы со­хра­ня­ем фо­то­гра­фии в пуб­лич­ном ка­та­ло­ге на внешнем уст­рой­ст­ве (SD-кар­те). Это оз­на­ча­ет, что ес­ли уда­лить при­ло­жение, фо­то­гра­фии оста­нут­ся. Ес­ли вы пред­по­чли бы их уда­лить, со­хра­няй­те их в ка­та­ло­ге, по­лу­чен­ном с по­мо­щью Context.getExternalFilesDir(). Од­на­ко уч­ти­те, что поль­зо­ва­те­ли мо­гут не ожи­дать то­го, что их фо­то­гра­фии бу­дут уда­ле­ны при уда­лении про­грам­мы ра­бо­ты с ка­ме­рой! Впро­чем, при неко­то­рых об­стоя­тель­ст­вах та­кое име­ет смысл – тща­тель­но про­ду­май­те это для сво­его при­ло­жения.

Ин­тер­фейс PictureCallback пре­достав­ля­ет доступ к дан­ным изо­бра­жения из про­грам­мы для ра­бо­ты с фо­то­гра­фия­ми; здесь мы по­лу­ча­ем файл, ку­да нуж­но за­пи­сать дан­ные (что по су­ти занима­ет боль­шую часть ко­да!), и за­пи­сы­ва­ем их в этот файл.

На­стро­ив PictureCallback, мы со­об­ща­ем ка­ме­ре, что нуж­но снять фо­то­гра­фию с по­мо­щью встро­ен­но­го ме­то­да takePicture(), и пе­ре­да­ем эти дан­ные PictureCallback при вы­зо­ве это­го ме­то­да (т. е. при на­жа­тии кноп­ки Capture).

При по­пыт­ке за­пустить про­грам­му сей­час вы уви­ди­те, что по­сле соз­дания сним­ка пред­ва­ри­тель­ный про­смотр «за­ви­са­ет» и боль­ше не ра­бо­та­ет. Что­бы это ис­пра­вить, до­бавь­те вы­зов camera.startPreview() по­сле соз­дания сним­ка.

Од­на­ко у нас оста­ет­ся дру­гая про­бле­ма: по­ка изо­бра­жение со­хра­ня­ет­ся в файл, пред­про­смотр бу­дет «за­мо­ро­жен».

Что­бы это ис­пра­вить, со­храним фо­то­гра­фию в фо­но­вом ре­жи­ме, соз­дав AsyncTask:

private void getImage() {

PictureCallback picture = new PictureCallback() {

public void onPictureTaken(byte[] data, Camera cam) {

new SaveImageTask().execute(data);

camera.startPreview();

}

};

camera.takePicture(null, null, picture);

}

class SaveImageTask extends AsyncTask<byte[], String, String> {

@Override protected String doInBackground(byte[]... data) {

File picFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);

if (picFile == null) {

Log.e(TAG, “Ошиб­ка при соз­да­нии ме­диа-фай­ла; вер­ны ли раз­ре­ше­ния на за­пись?”);

return null;

}

try {

// бло­ки try/catch, как сде­ла­но вы­ше...

}

return null;

}

}

Я рас­ска­зы­ва­ла о нем в пре­ды­ду­щей ста­тье; по су­ти это вспо­мо­га­тель­ный класс, от ко­то­ро­го мож­но унас­ле­до­вать­ся, что­бы лег­ко за­пустить про­цесс в фо­но­вой нити (обыч­но все, что за­пуска­ет­ся в ко­де, ра­бо­та­ет в од­ной основ­ной нити. Помните, что ин­тер­фейс Android небезо­па­сен с точ­ки зрения нитей, по­это­му вы­пол­няй­те в фоне толь­ко за­да­чи, не об­нов­ляю­щие гра­фи­че­­ский ин­тер­фейс!). В дан­ном слу­чае мы про­сто вы­но­сим в фон код со­хранения фай­ла, что­бы по­втор­но пе­ре­за­пустить про­смотр сра­зу по­сле соз­дания сним­ка.


Воз­мож­но­сти ка­ме­ры и эф­фек­ты

В ка­ме­рах со­вре­мен­ных смарт­фо­нов мо­жет быть мас­са до­полнитель­ных воз­мож­но­стей – вспыш­ка, ба­ланс бе­ло­го, ре­жим съем­ки бы­ст­ро дви­жу­щих­ся объ­ек­тов, да­же рас­по­зна­вание лиц (под­дер­жи­ва­ет­ся толь­ко в по­следнем ре­ли­зе Android). Что­бы уз­нать обо всех доступ­ных вам воз­мож­но­стях, изу­чи­те API. Для выяснения, ка­кие воз­мож­но­сти доступ­ны на ва­шем уст­рой­ст­ве, мож­но восполь­зо­вать­ся клас­сом Camera.Parameters. Уз­нав, ка­кие па­ра­мет­ры/воз­мож­но­сти вам доступ­ны, мо­же­те ра­бо­тать с ними еди­но­об­раз­но. Здесь я вклю­чу вспыш­ку; этот код лег­ко рас­ши­рить на дру­гие воз­мож­но­сти. Мы соз­да­дим ме­тод setUpFlash(), ко­то­рый вы­зы­ва­ет­ся из setUpLayout():

private void setUpFlash() {

final Camera.Parameters params = camera.getParameters();

final List<String> flashList = params.getSupportedFlashModes();

if (flashList == null) {

// Вспышки нет!

return;

}

final CharSequence[] flashCS = flashList.toArray(new CharSequence[flashList.size()]);

AlertDialog.Builder builder = new AlertDialog.Builder(this);

builder.setTitle(“Вы­бе­ри­те тип вспыш­ки”);

builder.setSingleChoiceItems(flashCS, -1,

new DialogInterface.OnClickListener() {

public void onClick(DialogInterface dialog, int which) {

params.setFlashMode(flashList.get(which));

camera.setParameters(params);

dialog.dismiss();

}

});

final AlertDialog alert = builder.create();

alert.show();

}

Спер­ва про­верь­те, есть ли на этой ка­ме­ре вспыш­ка, по­лу­чив Camera.Parameters для этой ка­ме­ры и под­дер­жи­вае­мые ре­жи­мы вспыш­ки из па­ра­мет­ров. Ес­ли ре­жи­мов нет, мы воз­вра­ща­ем­ся, ниче­го не де­лая.

Что­бы вклю­чить вспыш­ку, ис­поль­зу­ем AlertDialog (мож­но ис­поль­зо­вать и что-то дру­гое, на ва­ше пред­поч­тение). Диа­ло­гам AlertDialog ну­жен мас­сив CharSequence для по­лу­чения спи­ска ком­понен­тов, и нам нуж­но соз­дать его из спи­сков ре­жи­мов вспыш­ки. setSingleChoiceItems() соз­да­ет AlertDialog с ра­дио­кноп­ка­ми; ес­ли вам ну­жен спи­сок без ра­дио­кно­пок, восполь­зуй­тесь setItems().

На­конец, когда один из эле­мен­тов спи­ска вы­бран, мы за­да­ем со­от­вет­ст­вую­щий ре­жим вспыш­ки (уч­ти­те, что для по­лу­чения нуж­но­го объ­ек­та нам нуж­но вер­нуть­ся к flashList). Что­бы из­менения всту­пи­ли в си­лу, нуж­но вы­звать camera.setParameters(); без это­го па­ра­мет­ры не бу­дут уста­нов­ле­ны и ре­жим вспыш­ки не из­менит­ся, по­ка вы не сде­лае­те снимок. По­сле вы­бо­ра па­ра­мет­ра мы за­кры­ва­ем диа­лог для воз­вра­та в глав­ное ок­но.

Соз­дай­те и по­ка­жи­те диа­ло­го­вое ок­но, и все го­то­во! Ес­ли ском­пи­ли­ро­вать и за­пустить про­грам­му, вы уви­ди­те, что ок­но по­яв­ля­ет­ся сра­зу при за­пуске про­грам­мы, а это не то, что нам нуж­но. Во из­бе­жание это­го, до­бавь­те кноп­ку про­грамм­но (см. врез­ку).

Что­бы улуч­шить внешний вид про­грам­мы, ис­поль­зуй­те гра­фи­че­скую кноп­ку вме­сто тек­сто­вой; кро­ме то­го, есть еще мас­са воз­мож­но­стей, с ко­то­ры­ми мож­но по­экс­пе­ри­мен­ти­ро­вать при же­лании. Ос­та­ет­ся толь­ко во­прос, что де­лать со все­ми эти­ми сним­ка­ми... |


Персональные инструменты
купить
подписаться
Яндекс.Метрика