読む時間がない場合、または情報を知っている場合は、Androidスマートフォンのカメラからフルサイズの画像を取得するための最終的なコードが記事の最後にあります。
問題の説明
あなたは、クロスプラットフォームのアプリケーションを作成する場合は、使用することができQCameraのPC用から画像を取得するためにクラスをカメラ、例えば、 Qtのドキュメントに記述されています。
上記の例に従って、.proファイルに追加します
QT += multimedia multimediawidgets
次に、Webカメラからの画像を表示し、将来使用するためにQPixmapまたはQImageに保存するウィジェットをプログラムで作成します。
Androidで同じことを行うタスクが発生すると、マルチメディアウィジェットはこのOSでサポートされておらず、カメラは写真を撮影して保存しますが、QCameraViewfinderはマルチメディアウィジェットを使用し、使用しないため、現時点で表示される内容は謎になります。Androidには何も表示されません。問題の解決策をさらに検索すると、次の2つの解決策が得られます。
QMLを使用して、この機能を実行する独自のQtクイック要素を作成し、それをQtウィジェット、C ++でアプリケーションの残りの部分とドッキングします。
デフォルトのAndroidスマートフォンアプリケーションを使用して写真を受信し、アプリケーションで処理します。
最初のオプションを検討してください
QtウィジェットのC ++プログラマーの場合、QMLの次のエピソードの深化には時間がかかります。この時間を追加して、Qtクイック要素を記述し、この要素をC ++コードとドッキングし、記述されたコードをデバッグします。QMLの専門家でない場合は、長くて難しいことがわかります。
2番目のオプションを検討してください
Android- -, , , Java- (JNI — Java Native Interface) ++ QtAndroid. . , , , Android .
, Android- .
Android , . , GitHub , . , , FileUriExposedException , .
, , , Java- — .
.pro .
Android.
android {
QT +=androidextras
}
, QAndroidActivityResultReceiver. , , Qt, QObject.
(.h) :
#ifndef CAMSHOT_H
#define CAMSHOT_H
#include <QObject>
#include <QString>
#include <cstring>
#include <QImage>
#include <QDebug>
#include <QtAndroid>
#include <QAndroidActivityResultReceiver>
#include <QAndroidParcel>
class CamShot : public QObject, public QAndroidActivityResultReceiver
{
Q_OBJECT
public:
CamShot(QObject *parent = nullptr):QObject(parent),QAndroidActivityResultReceiver(){}
static const int RESULT_OK = -1;
static const int REQUEST_IMAGE_CAPTURE = 1;
static const int REQUEST_TAKE_PHOTO = REQUEST_IMAGE_CAPTURE;
void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override;
static QImage camThumbnailToQImage(const QAndroidJniObject &data);
public slots:
void aMakeShot();
signals:
void createNew(const QImage &img);
};
#endif // CAMSHOT_H
(.cpp) :
QImage CamShot::camThumbnailToQImage(const QAndroidJniObject &data){
QAndroidJniObject bundle = data.callObjectMethod("getExtras","()Landroid/os/Bundle;");
qDebug()<<"bundle.isValid() "<<bundle.isValid()<<bundle.toString();
QAndroidJniObject bundleKey = QAndroidJniObject::fromString("data");
const QAndroidJniObject aBitmap (data.callObjectMethod("getParcelableExtra", "(Ljava/lang/String;)Landroid/os/Parcelable;", bundleKey.object<jstring>()));
qDebug()<<"aBitmap.isValid() "<<aBitmap.isValid()<<aBitmap.toString();
jint aBitmapWidth = aBitmap.callMethod<jint>("getWidth");
jint aBitmapHeight = aBitmap.callMethod<jint>("getHeight");
QAndroidJniEnvironment env;
const int32_t aBitmapPixelsCount = aBitmapWidth * aBitmapHeight;
jintArray pixels = env->NewIntArray(aBitmapPixelsCount);
jint aBitmapOffset = 0;
jint aBitmapStride = aBitmapWidth;
jint aBitmapX = 0;
jint aBitmapY = 0;
aBitmap.callMethod<void>("getPixels","([IIIIIII)V", pixels, aBitmapOffset, aBitmapStride, aBitmapX, aBitmapY, aBitmapWidth, aBitmapHeight);
jint *pPixels = env->GetIntArrayElements(pixels, nullptr);
QImage img(aBitmapWidth, aBitmapHeight, QImage::Format_ARGB32);
int lineSzB = aBitmapWidth * sizeof(jint);
for (int i = 0; i < aBitmapHeight; ++i){
uchar *pDst = img.scanLine(i);
const uchar *pSrc = reinterpret_cast<const uchar*>(pPixels + aBitmapWidth * i + aBitmapWidth);
memcpy(pDst, pSrc, lineSzB);
}
env->DeleteLocalRef(pixels); //env->ReleaseIntArrayElements(pixels, pPixels, 0); , , DeleteLocalRef.
return img;
}
void CamShot::aMakeShot() {
QAndroidJniObject action = QandroidJniObject::fromString("android.media.action.IMAGE_CAPTURE");
// Java- ( ), (- "/"), "android/content/Intent", "java/lang/String".
// Java-, "L" ";" , "Landroid/content/Intent ;", "Ljava/lang/String;".
// , , "V" (void) "[IIIIIII" ( jint, 6 jint )
//, :
QAndroidJniObject intent=QAndroidJniObject("android/content/Intent","(Ljava/lang/String;)V", action.object<jstring>());
QtAndroid::startActivity(intent, REQUEST_IMAGE_CAPTURE, this);
}
void CamShot::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data){
if ( receiverRequestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK )
{
const QImage thumbnail (camThumbnailToQImage(data));
if (!thumbnail.isNull())
emit createNew(thumbnail);
}
}
JNI-
Java- ( Java-), (- "/"), "android/content/Intent", "java/lang/String";
Java-, "L" ";" , "Landroid/content/Intent;", "Ljava/lang/String;";
, ( ), "V" (void), "I" (jint) "[IIIIIII" ( jint, 6 jint );
:
C/C++
JNI
Java
Signature
uint8_t/unsigned char
jboolean
bool
Z
int8_t/char/signed char
jbyte
byte
B
uint16_t/unsigned short
jchar
char
C
int16_t/short
jshort
short
S
int32_t/int/(long)
jint
int
I
int64_t/(long)/long long
jlong
long
J
float
jfloat
float
F
double
jdouble
double
D
void
void
V
:
JNI
Java
Signature
jbooleanArray
bool[]
[Z
jbyteArray
byte[]
[B
jcharArray
char[]
[C
jshortArray
short[]
[S
jintArray
int[]
[I
jlongArray
long[]
[L
jfloatArray
float[]
[F
jdoubleArray
double[]
[D
jarray
type[]
[Lfully/qualified/type/name;
jarray
String[]
[Ljava/lang/String;
, JNI- QAndroidJniEnvironment, : NewIntArray, GetIntArrayElements, DeleteLocalRef GetArrayLength,GetObjectArrayElement, SetObjectArrayElement, ..
(pdf) Practical Qt on Android JNI — qtcon.
class CamShot :
, Android ( Java-);
void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override;
Java- Intent ;
static QImage camThumbnailToQImage(const QAndroidJniObject &data);
Java- Intent Java- Bitmap, (32- ) QImage;
void aMakeShot();
;
void createNew(const QImage &img);
.
void aMakeShot() Java- Intent , , — . (Intent) (Activity).
- . , handleActivityResult, : . , camThumbnailToQImage QImage Java- Bitmap Qt.
static QImage camThumbnailToQImage(const QAndroidJniObject &data) override;
Java- Intent Java- Bundle, Intent:
Bundle getExtras()
Bundle <->:<>. Android , . "data".
Java- Bitmap , Intent:
T getParcelableExtra (String name)
C Bitmap QImage , . . Bitmap . ( ) QImage .
Bitmap QImage Bitmap:
void getPixels (int[] pixels, int offset, int stride, int x, int y, int width, int height)
jintArray pixels = env->NewIntArray(aBitmapPixelsCount);
, , , C++ :
jint *pPixels = env->GetIntArrayElements(pixels, nullptr);
Qimage. ,
env->DeleteLocalRef(pixels);
QImage.
. .
FileProvider, Uri . , Android, , :
androidx.core.content.FileProvider;
android.support.v4.content.FileProvider.
— , Qt, QtCreator:
()→ «» → «» → «»→ «Android»→ «SDK Manager»→ «» → «Extras»→ «Android Support Repository» - «» .
Android
QtCreator «». « »→ «». «Build Android APK» → «Create Templates». « Gradle Android», «»:
«android», .
Android
- Android, .pro android: :
android { QT +=androidextras } # … DISTFILES += \ android: android/AndroidManifest.xml \ android: android/build.gradle \ android: android/gradle/wrapper/gradle-wrapper.jar \ android: android/gradle/wrapper/gradle-wrapper.properties \ android: android/gradlew \ android: android/gradlew.bat \ android: android/res/values/libs.xml \ todo.txt
AndroidManifest.xml
«AndroidManifest.xml» android/AndroidManifest.xml,
</activity>
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices ->
:
<provider android:name="android.support.v4.content.FileProvider" android:authorities="org.qtproject.example.qsketch.fileprovider" android:grantUriPermissions="true" android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/>
</provider>
, - .
, «AndroidManifest.xml» «res» «values», «xml», «file_paths.xml» (… /abin/AndroidManifest.xml) (… /abin/res/xml/file_paths.xml). :
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="shared" path="shared/" />
</paths>
shared/
, FileProvider
android/build.gradle, dependencies :
compile'com.android.support:support-v4:25.3.1'
:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) compile'com.android.support:support-v4:25.3.1' }
, Android Support Repository.
Sharing Files on Android or iOS from or with your Qt App - Part 4 .
(.h) :
#ifndef CAMSHOT_H
#define CAMSHOT_H
#include <QObject>
#include <QImage>
#include <QString>
#include <QDebug>
#include <QtAndroid>
#include <QAndroidActivityResultReceiver>
#include <QAndroidParcel>
#include "auxfunc.h"
class CamShot : public QObject, public QAndroidActivityResultReceiver
{
Q_OBJECT
public:
static const int RESULT_OK = -1;
static const int REQUEST_IMAGE_CAPTURE = 1;
static const int REQUEST_TAKE_PHOTO = REQUEST_IMAGE_CAPTURE;
enum ImgOrientation {ORIENTATION_UNDEFINED = 0, ORIENTATION_NORMAL = 1, ORIENTATION_FLIP_HORIZONTAL = 2, ORIENTATION_ROTATE_180 = 3, ORIENTATION_FLIP_VERTICAL = 4, ORIENTATION_TRANSPOSE = 5,
ORIENTATION_ROTATE_90 = 6, ORIENTATION_TRANSVERSE = 7, ORIENTATION_ROTATE_270 = 8};
void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override;
static QImage aBitmapToQImage(const QAndroidJniObject &aBitmap);
static QImage camThumbnailToQImage(const QAndroidJniObject &data);
ImgOrientation needRotateAtRightAngle();
QImage camImageToQImage();
static void applyOrientation(QImage &img, const ImgOrientation &orientation);
explicit CamShot(QObject *parent = nullptr):QObject(parent),QAndroidActivityResultReceiver(){}
~CamShot();
private:
QAndroidJniObject tempImgURI;
QAndroidJniObject tempImgFile;
QAndroidJniObject tempImgAbsPath;
bool _thumbnailNotFullScaleRequested;
public slots:
void aMakeShot(const bool &thumbnailNotFullScale = false);
signals:
void createNew(const QImage &img);
};
#endif // CAMSHOT_H
(.cpp) :
QImage CamShot::aBitmapToQImage(const QAndroidJniObject &aBitmap){
if (!aBitmap.isValid())
return QImage();
jint aBitmapWidth = aBitmap.callMethod<jint>("getWidth");
jint aBitmapHeight = aBitmap.callMethod<jint>("getHeight");
QAndroidJniEnvironment env;
const int32_t aBitmapPixelsCount = aBitmapWidth * aBitmapHeight;
jintArray pixels = env->NewIntArray(aBitmapPixelsCount);
jint aBitmapOffset = 0;
jint aBitmapStride = aBitmapWidth;
jint aBitmapX = 0;
jint aBitmapY = 0;
aBitmap.callMethod<void>("getPixels","([IIIIIII)V", pixels, aBitmapOffset, aBitmapStride, aBitmapX, aBitmapY, aBitmapWidth, aBitmapHeight);
jint *pPixels = env->GetIntArrayElements(pixels, nullptr);
QImage img(aBitmapWidth, aBitmapHeight, QImage::Format_ARGB32);
int lineSzB = aBitmapWidth * sizeof(jint);
for (int i = 0; i < aBitmapHeight; ++i){
uchar *pDst = img.scanLine(i);
const uchar *pSrc = reinterpret_cast<const uchar*>(pPixels + aBitmapWidth * i + aBitmapWidth);
memcpy(pDst, pSrc, lineSzB);
}
env->DeleteLocalRef(pixels); //env->ReleaseIntArrayElements(pixels, pPixels, 0); , , DeleteLocalRef.
return img;
}
QImage CamShot::camThumbnailToQImage(const QAndroidJniObject &data){
//
QAndroidJniObject bundle = data.callObjectMethod("getExtras","()Landroid/os/Bundle;");
qDebug()<<"bundle.isValid() "<<bundle.isValid()<<bundle.toString();
// jstring ( Java) "data" - <, > - Bitmap (Java)
QAndroidJniObject bundleKey = QAndroidJniObject::fromString("data");
// "data" : Bitmap
const QAndroidJniObject aBitmap (data.callObjectMethod("getParcelableExtra", "(Ljava/lang/String;)Landroid/os/Parcelable;", bundleKey.object<jstring>()));
qDebug()<<"aBitmap.isValid() "<<aBitmap.isValid()<<aBitmap.toString();
return aBitmapToQImage(aBitmap);
}
QImage CamShot::camImageToQImage(){
QAndroidJniObject bitmap = QAndroidJniObject::callStaticObjectMethod("android/graphics/BitmapFactory","decodeFile","(Ljava/lang/String;)Landroid/graphics/Bitmap;",tempImgAbsPath.object<jobject>());
qDebug()<<"bitmap.isValid() "<<bitmap.isValid()<<bitmap.toString();
QImage img = aBitmapToQImage(bitmap);
//
if (tempImgFile.isValid())
tempImgFile.callMethod<jboolean>("delete");
return img;
}
CamShot::ImgOrientation CamShot::needRotateAtRightAngle(){
//
QAndroidJniObject exifInterface = QAndroidJniObject("android/media/ExifInterface","(Ljava/lang/String;)V",
tempImgAbsPath.object<jstring>());
qDebug() << __FUNCTION__ << "exifInterface.isValid()=" << exifInterface.isValid();
QAndroidJniObject TAG_ORIENTATION = QAndroidJniObject::getStaticObjectField<jstring>("android/media/ExifInterface", "TAG_ORIENTATION");
qDebug() << __FUNCTION__ << "TAG_ORIENTATION.isValid()=" << TAG_ORIENTATION.isValid()<<TAG_ORIENTATION.toString();
const jint orientation = exifInterface.callMethod<jint>("getAttributeInt","(Ljava/lang/String;I)I",TAG_ORIENTATION.object<jstring>(),static_cast<jint>(ORIENTATION_UNDEFINED));
return static_cast<ImgOrientation>(orientation);
}
void CamShot::applyOrientation(QImage &img, const ImgOrientation &orientation){
switch (orientation){
case ORIENTATION_UNDEFINED:
case ORIENTATION_NORMAL:
break;
case ORIENTATION_FLIP_HORIZONTAL:{
img = img.mirrored(true, false);
break;
}
case ORIENTATION_ROTATE_180:
Aux::rotateImgCW180(img);
break;
case ORIENTATION_FLIP_VERTICAL:{
img = img.mirrored(false, true);
break;
}
case ORIENTATION_TRANSPOSE:{
img = img.mirrored(true, false);
Aux::rotateImgCW270(img);
break;
}
case ORIENTATION_ROTATE_90:
Aux::rotateImgCW90(img);
break;
case ORIENTATION_TRANSVERSE:{
img = img.mirrored(true, false);
Aux::rotateImgCW90(img);
break;
}
break;
case ORIENTATION_ROTATE_270:
Aux::rotateImgCW270(img);
break;
}
}
void CamShot::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data){
if ( receiverRequestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK )
{
if (_thumbnailNotFullScaleRequested){
const QImage thumbnail (camThumbnailToQImage(data));
if (!thumbnail.isNull())
emit createNew(thumbnail);
return;
}
const ImgOrientation orientation = needRotateAtRightAngle();
QImage image (camImageToQImage());
if (!image.isNull()){
applyOrientation(image, orientation);
emit createNew(image);
}
}
}
void CamShot::aMakeShot(const bool &thumbnailNotFullScale) {
QAndroidJniObject action = QAndroidJniObject::fromString("android.media.action.IMAGE_CAPTURE");
//
QAndroidJniObject intent=QAndroidJniObject("android/content/Intent","(Ljava/lang/String;)V",
action.object<jstring>());
qDebug() << __FUNCTION__ << "intent.isValid()=" << intent.isValid();
_thumbnailNotFullScaleRequested = thumbnailNotFullScale;
if (thumbnailNotFullScale) {
//
QtAndroid::startActivity(intent, REQUEST_IMAGE_CAPTURE, this);
return;
}
//
QAndroidJniObject context = QtAndroid::androidContext();
QString contextStr (context.toString());
qDebug() <<"Context: "<<contextStr;
//
QAndroidJniObject extDir = context.callObjectMethod("getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;", NULL);
qDebug() << __FUNCTION__ << "extDir.isValid()=" << extDir.isValid()<<extDir.toString();
//
QAndroidJniObject extDirAbsPath = extDir.callObjectMethod("getAbsolutePath","()Ljava/lang/String;");
// . . /res/xml/file_paths.xml
extDirAbsPath = QAndroidJniObject::fromString(extDirAbsPath.toString() + "/shared");
const QString extDirAbsPathStr = extDirAbsPath.toString();
qDebug() << __FUNCTION__ << "extDirAbsPath.isValid()=" << extDirAbsPath.isValid()<<extDirAbsPathStr;
//
QAndroidJniObject sharedFolder=QAndroidJniObject("java.io.File","(Ljava/lang/String;)V",
extDirAbsPath.object<jstring>());
qDebug() << __FUNCTION__ << "sharedFolder.isValid()=" << sharedFolder.isValid()<<sharedFolder.toString();
const jboolean sharedFolderCreated = sharedFolder.callMethod<jboolean>("mkdirs");
Q_UNUSED(sharedFolderCreated);
// ,
//
QAndroidJniObject suggestedFilePath = QAndroidJniObject::fromString(extDirAbsPathStr+"/"+"_tmp.jpg");
qDebug() << __FUNCTION__ << "suggestedFilePath.isValid()=" << suggestedFilePath.isValid()<<suggestedFilePath.toString();
//
//
QAndroidJniObject tempImgFile=QAndroidJniObject("java.io.File","(Ljava/lang/String;)V",
suggestedFilePath.object<jstring>());
qDebug() << __FUNCTION__ << "fileExistsCheck.isValid()=" << tempImgFile.isValid()<<tempImgFile.toString();
// ,
if (tempImgFile.isValid()){
const jboolean deleted = tempImgFile.callMethod<jboolean>("delete");
Q_UNUSED(deleted);
}
//
const jboolean fileCreated = tempImgFile.callMethod<jboolean>("createNewFile");
Q_UNUSED(fileCreated);
//
tempImgAbsPath = tempImgFile.callObjectMethod("getAbsolutePath","()Ljava/lang/String;");
qDebug() << __FUNCTION__ << "tempImgAbsPath.isValid()=" << tempImgAbsPath.isValid()<<tempImgAbsPath.toString();
// authority fileprovider
const QString contextFileProviderStr ("org.qtproject.example.qsketch.fileprovider");
const char androidFileProvider [] = "android/support/v4/content/FileProvider";
//const char androidxFileProvider [] = "androidx/core/content/FileProvider"; - Qt
/*QAndroidJniObject*/ tempImgURI = QAndroidJniObject::callStaticObjectMethod(androidFileProvider, "getUriForFile", "(Landroid/content/Context;Ljava/lang/String;Ljava/io/File;)Landroid/net/Uri;",
context.object<jobject>(), QAndroidJniObject::fromString(contextFileProviderStr).object<jstring>(), tempImgFile.object<jobject>());
qDebug() << __FUNCTION__ << "tempImgURI.isValid()=" << tempImgURI.isValid()<<tempImgURI.toString();
// MediaStore.EXTRA_OUTPUT
QAndroidJniObject MediaStore__EXTRA_OUTPUT
= QAndroidJniObject::getStaticObjectField("android/provider/MediaStore", "EXTRA_OUTPUT", "Ljava/lang/String;");
qDebug() << "MediaStore__EXTRA_OUTPUT.isValid()=" << MediaStore__EXTRA_OUTPUT.isValid();
// URI
intent.callObjectMethod("putExtra","(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;",MediaStore__EXTRA_OUTPUT.object<jstring>(), tempImgURI.object<jobject>());
qDebug() << __FUNCTION__ << "intent.isValid()=" << intent.isValid();
QtAndroid::startActivity(intent, REQUEST_IMAGE_CAPTURE, this);
}
- Aux .
(.h) Aux :
#ifndef AUXFUNC_H
#define AUXFUNC_H
#include <QImage>
#include <QColor>
#include <QPainter>
#include <QMatrix>
#include <QSize>
#include <QPoint>
class Aux
{
public:
static void resizeCenteredImg(QImage *image, const QSize &newSize, const QColor bgColor);
static void rotateImg(QImage &img, qreal degrees);
static void rotateImgCW90(QImage &img);
static void rotateImgCW180(QImage &img);
static void rotateImgCW270(QImage &img);
};
#endif // AUXFUNC_H
(.cpp) Aux :
void Aux::resizeCenteredImg(QImage *image, const QSize &newSize, const QColor bgColor){
if (image->size() == newSize)
return;
const QSize szDiff = newSize - image->size();
QImage newImage(newSize, QImage::Format_ARGB32);
newImage.fill(bgColor);
QPainter painter(&newImage);
painter.drawImage(QPoint(szDiff.width()/2, szDiff.height()/2), *image);
*image = newImage;
}
void Aux::rotateImg(QImage &img, qreal degrees){
QPoint center = img.rect().center();
QMatrix matrix;
matrix.translate(center.x(), center.y());
matrix.rotate(degrees);
img = img.transformed(matrix, Qt::SmoothTransformation);
}
void Aux::rotateImgCW90(QImage &img){
const int w = img.width();
const int h = img.height();
const int maxDim = std::max(w, h);
resizeCenteredImg(&img, QSize(maxDim, maxDim), Qt::white);
rotateImg(img, 90);
resizeCenteredImg(&img, QSize(h, w), Qt::white);
}
void Aux::rotateImgCW180(QImage &img){
rotateImg(img, 180);
}
void Aux::rotateImgCW270(QImage &img){
const int w = img.width();
const int h = img.height();
const int maxDim = std::max(w, h);
resizeCenteredImg(&img, QSize(maxDim, maxDim), Qt::white);
rotateImg(img, 270);
resizeCenteredImg(&img, QSize(h, w), Qt::white);
}
.
«thumbnailNotFullScale». , , , . JNI-.
サムネイルが常に正しい向きである場合、フルサイズの画像は一方向に向けられているため、回転させる必要があります。必要な変換についての情報は、使用した画像のEXIF特性から得ることができるExifInterfaceを。インターネットで見つかったJavaの例では、通常の方向への変換はJavaコードで行われます。Qtの場合、デバッグが難しく面倒なJNI呼び出しで自分を苦しめる意味はなく、すべてを実行する方が簡単です。 Qtで必要な変換。